Каждый, кто приходит в Go из других языков (например: Python, Java или C), сталкивается с вопросом: почему для форматирования даты используется строка "2006-01-02 15:04:05", а не человеко-читаемые %Y-%m-%d %H:%M:%S?
Это не баг и не злая шутка. Это осознанное решение в дизайне языка, которое после понимания логики оказывается удобнее классических спецификаторов.
Как это работает в других языках
В большинстве языков программирования форматирование дат построено на спецификации форматирование, унаследованных ещё от функции strftime языка C (1989 год):
# Python
from datetime import datetime
now = datetime.now()
print(now.strftime("%Y-%m-%d %H:%M:%S"))
# 2026-03-07 14:30:00
У этого подхода есть проблема: вам нужно помнить, что:
%Y= четырёхзначный год%m= месяц с ведущим нулём%M= минуты (а не месяц!)%S= секунды.
Перепутать регистр или букву легко, а ошибка проявится только в рантайме.
Идея Go: reference time
В Go вместо символьных спецификаторов используется конкретная дата-пример, называемая reference time (с англ. “исходное время”):
Mon Jan 2 15:04:05 MST 2006
Или в числовом виде:
01/02 03:04:05PM '06 -0700
Каждый компонент этой даты имеет уникальное числовое значение:
| Компонент | Значение | Что обозначает |
|---|---|---|
| Месяц | 01 | Январь (1-й месяц) |
| День | 02 | 2-е число |
| Час | 03 или 15 | 3 часа дня: 03 для 12-часового, 15 для 24-часового формата |
| Минуты | 04 | 4 минуты |
| Секунды | 05 | 5 секунд |
| Год | 06 (2006) | 2006 год |
| Часовой пояс | -0700 | Смещение UTC-7 |
Последовательность 1, 2, 3, 4, 5, 6, 7. Месяц = 1, день = 2, час = 3, минуты = 4, секунды = 5, год = 6, часовой пояс = 7.
Чтобы отформатировать дату, вы просто пишете пример того, как должен выглядеть результат, подставляя компоненты reference time:
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
// Формат ISO 8601
fmt.Println(now.Format("2006-01-02T15:04:05Z07:00"))
// 2026-03-07T14:30:00+03:00
// Только дата
fmt.Println(now.Format("2006-01-02"))
// 2026-03-07
// Человекочитаемый формат
fmt.Println(now.Format("02 Jan 2006"))
// 07 Mar 2026
// Время в 12-часовом формате
fmt.Println(now.Format("3:04 PM"))
// 2:30 PM
}
Метод Format смотрит на строку-шаблон и заменяет каждый распознанный компонент reference time на соответствующее значение из переданной даты. Всё, что не является компонентом reference time (дефисы, двоеточия, пробелы, буква T и прочие символы), остаётся как есть.
Обратите внимание на формат Z07:00 в первом примере. Префикс Z не литерал, а условный оператор: если время в UTC, Format выведет букву Z (как требует ISO 8601), иначе числовое смещение вроде +03:00. Формат -07:00 (без Z) всегда выводит числовое смещение, даже для UTC (+00:00):
utc := time.Date(2026, 3, 7, 14, 30, 0, 0, time.UTC)
fmt.Println(utc.Format("2006-01-02T15:04:05Z07:00"))
// 2026-03-07T14:30:00Z
fmt.Println(utc.Format("2006-01-02T15:04:05-07:00"))
// 2026-03-07T14:30:00+00:00
Все компоненты формата
Пакет time стандартной библиотеки определяет полный набор компонентов:
Год
| Шаблон | Результат | Пример |
|---|---|---|
2006 | Четырёхзначный год | 2026 |
06 | Двузначный год | 26 |
Месяц
| Шаблон | Результат | Пример |
|---|---|---|
January | Полное название (англ.) | March |
Jan | Сокращённое название (англ.) | Mar |
01 | Номер с ведущим нулём | 03 |
1 | Номер без ведущего нуля | 3 |
День
| Шаблон | Результат | Пример |
|---|---|---|
Monday | Полное название дня недели (англ.) | Friday |
Mon | Сокращённое название (англ.) | Fri |
02 | День с ведущим нулём | 07 |
2 | День без ведущего нуля | 7 |
_2 | День с ведущим пробелом | 7 |
Час
| Шаблон | Результат | Пример |
|---|---|---|
15 | 24-часовой формат | 14 |
03 | 12-часовой с ведущим нулём | 02 |
3 | 12-часовой без ведущего нуля | 2 |
PM | Маркер AM/PM (верхний регистр) | PM |
pm | Маркер am/pm (нижний регистр) | pm |
Минуты и секунды
| Шаблон | Результат | Пример |
|---|---|---|
04 | Минуты с ведущим нулём | 05 |
4 | Минуты без ведущего нуля | 5 |
05 | Секунды с ведущим нулём | 09 |
5 | Секунды без ведущего нуля | 9 |
Доли секунды
Шаблоны с нулями (0) всегда выводят указанное количество знаков, дополняя нулями справа. Шаблоны с девятками (9) отбрасывают незначащие нули в конце:
| Шаблон | Результат | Пример (для .123456789) |
|---|---|---|
.000 | 3 знака (с нулями) | .123 |
.000000 | 6 знаков (с нулями) | .123456 |
.000000000 | 9 знаков (с нулями) | .123456789 |
.9 | 1 знак (без trailing zeros) | .1 |
.99 | 2 знака (без trailing zeros) | .12 |
.999 | 3 знака (без trailing zeros) | .123 |
Часовой пояс
Форматы с префиксом Z выводят букву Z для UTC и числовое смещение для остальных зон. Форматы с - всегда выводят числовое смещение:
| Шаблон | Результат | Пример (MSK) | Пример (UTC) |
|---|---|---|---|
MST | Аббревиатура | MSK | UTC |
Z07:00 | Z или ±hh:mm | +03:00 | Z |
Z0700 | Z или ±hhmm | +0300 | Z |
Z07 | Z или ±hh | +03 | Z |
-07:00 | ±hh:mm | +03:00 | +00:00 |
-0700 | ±hhmm | +0300 | +0000 |
-07 | ±hh | +03 | +00 |
Предопределённые константы
Пакет time предоставляет готовые форматов для распространённых стандартов:
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
fmt.Println(now.Format(time.RFC3339))
// 2026-03-07T14:30:00+03:00
fmt.Println(now.Format(time.RFC822))
// 07 Mar 26 14:30 MSK
fmt.Println(now.Format(time.Kitchen))
// 2:30PM (формат "3:04PM", 12-часовой без пробела перед AM/PM)
fmt.Println(now.Format(time.RFC3339Nano))
// 2026-03-07T14:30:00.123456789+03:00 (RFC3339 с наносекундами (trailing zeros убираются))
fmt.Println(now.Format(time.DateOnly))
// 2026-03-07
fmt.Println(now.Format(time.TimeOnly))
// 14:30:00
fmt.Println(now.Format(time.DateTime))
// 2026-03-07 14:30:00
}
Полный список можно посмотреть в документации пакета time.
Константы time.DateOnly, time.TimeOnly и time.DateTime появились в Go 1.20 для упрощения большинство повседневных задач. Оригинальные issue на GitHub #52746.
Парсинг дат
Тот же принцип reference time работает при парсинге time.Time из строки через функцию time.Parse:
package main
import (
"fmt"
"time"
)
func main() {
// Парсинг даты из строки
t, err := time.Parse("2006-01-02", "2026-03-07")
if err != nil {
fmt.Println("ошибка парсинга:", err)
return
}
fmt.Println(t)
// 2026-03-07 00:00:00 +0000 UTC
// Парсинг с часовым поясом
t2, err := time.Parse(time.RFC3339, "2026-03-07T14:30:00+03:00")
if err != nil {
fmt.Println("ошибка парсинга:", err)
return
}
fmt.Println(t2)
// 2026-03-07 14:30:00 +0300 +0300
}
При выборе между Parse и ParseInLocation опирайтесь на то, содержит ли входная строка информацию о часовом поясе. Если да (например, +03:00 или Z), time.Parse извлечёт зону из самой строки. Если нет, то Parse вернёт время в UTC, что может быть неожиданным. В таком случае используйте ParseInLocation, чтобы явно указать нужную локацию:
// Строка без информации о часовом поясе
loc, _ := time.LoadLocation("Europe/Moscow")
t, err := time.ParseInLocation("2006-01-02 15:04", "2026-03-07 14:30", loc)
Типичные ошибки
Перепутаны компоненты reference time
Использование произвольных чисел вместо компонентов reference time приведёт к ошибке:
_, err := time.Parse("2024-12-25", "2026-03-07")
fmt.Println(err)
// parsing time "2026-03-07" as "2024-12-25": cannot parse "-03-07" as "4"
Парсер внутри time.Parse интерпретирует строку формата покомпонентно: сначала пытается распознать в "2024-12-25" токены reference time и литералы. Числа 24, 12, 25 не соответствуют ожидаемым компонентам, и парсер выдаёт ошибку в точке, где не может сопоставить остаток строки. Ошибки в таких случаях выглядят загадочно, поэтому при отладке стоит в первую очередь проверить, используете ли вы компоненты reference time. Внимательность к использованию правильного порядка аргументов является единственным решением:
t, err := time.Parse("2006-01-02", "2026-03-07")
Перепутаны день и месяц
Формат сломается, если вместо 01 (месяц) написать 02 (день) или наоборот:
// Ожидаем формат DD/MM/YYYY
// Неправильно: 01 = месяц, 02 = день
t, _ := time.Parse("01/02/2006", "07/03/2026")
fmt.Println(t) // 2026-07-03 (месяц и день поменялись!)
// Правильно: 02 = день, 01 = месяц
t, _ = time.Parse("02/01/2006", "07/03/2026")
fmt.Println(t) // 2026-03-07
Хорошая новость: go vet умеет ловить подобные ошибки. Если вы напишете "2006-02-01" (день и месяц в обратном порядке по сравнению со стандартным ISO-форматом), go vet предупредит:
$ go vet ./...
./main.go:11:26: 2006-02-01 should be 2006-01-02
12-часовой и 24-часовой формат
Если используете 03 (12-часовой формат) без PM/AM, результат может быть неожиданным:
// 15 = 24-часовой формат
fmt.Println(now.Format("15:04:05"))
// 14:30:00
// 3 = 12-часовой формат (без AM/PM непонятно, день или ночь)
fmt.Println(now.Format("3:04:05"))
// 2:30:00
// Правильно для 12-часового формата
fmt.Println(now.Format("3:04:05 PM"))
// 2:30:00 PM
Числа reference time в произвольном тексте
Что если вам нужно вывести строку, содержащую число 2006 как литерал, а не как год?
now := time.Date(2026, 3, 7, 14, 30, 0, 0, time.UTC)
fmt.Println(now.Format("The year is 2006"))
// The year is 2026
Format всегда интерпретирует 2006 как компонент года. Экранирования или кавычек для литералов в Go нет. Если в строке формата случайно окажутся числа, совпадающие с компонентами reference time (01, 02, 03, 04, 05, 06, 15), они будут заменены на реальные значения. Для вставки произвольного текста рядом с датой используйте fmt.Sprintf:
formatted := fmt.Sprintf("The year is %s", now.Format("2006"))
// The year is 2026 (контроль над тем, что является форматом, а что литералом)
Строгая проверка разделителей при парсинге
time.Parse проверяет не только числовые компоненты, но и все разделители: дефисы, слэши, пробелы, двоеточия. Если формат ожидает дефис, а во входной строке стоит слэш, парсинг завершится ошибкой:
_, err := time.Parse("2006-01-02", "2006/01/02")
fmt.Println(err)
// parsing time "2006/01/02" as "2006-01-02": cannot parse "/01/02" as "-"
Это важно при работе с «грязными» данными от пользователей или внешних API, где формат даты может варьироваться. Go не пытается «угадать» формат. От вас потребуется точное понимание требуемого/требуемых разделителей.
Format работает только с моментами времени
Format — это метод типа time.Time, то есть конкретного момента во времени. У time.Duration (интервал, длительность) метода Format нет:
d := 2*time.Hour + 3*time.Minute + 45*time.Second
fmt.Println(d)
// 2h3m45s
// d.Format("15:04:05") не скомпилируется
Если нужно вывести длительность в формате HH:MM:SS, собирайте строку вручную:
h := int(d.Hours())
m := int(d.Minutes()) % 60
s := int(d.Seconds()) % 60
fmt.Sprintf("%02d:%02d:%02d", h, m, s)
// 02:03:45
Потеря часового пояса при парсинге
time.Parse без информации о часовом поясе возвращает время в UTC:
t, _ := time.Parse("2006-01-02 15:04", "2026-03-07 14:30")
fmt.Println(t.Location()) // UTC
// Используйте ParseInLocation для парсинга в конкретном часовом поясе
loc, _ := time.LoadLocation("Europe/Moscow")
t, _ = time.ParseInLocation("2006-01-02 15:04", "2026-03-07 14:30", loc)
fmt.Println(t.Location()) // Europe/Moscow
Шпаргалка: strftime vs Go
Если вы переходите из Python или C, эта таблица поможет быстро найти аналог:
| strftime | Go | Описание |
|---|---|---|
%Y | 2006 | Год (4 цифры) |
%y | 06 | Год (2 цифры) |
%m | 01 | Месяц (01-12) |
%d | 02 | День (01-31) |
%H | 15 | Час (00-23) |
%I | 03 | Час (01-12) |
%M | 04 | Минуты (00-59) |
%S | 05 | Секунды (00-59) |
%p | PM | AM/PM |
%Z | MST | Часовой пояс |
%z | -0700 | Смещение UTC |
%A | Monday | День недели |
%a | Mon | День недели (сокр.) |
%B | January | Месяц (полное) |
%b | Jan | Месяц (сокр.) |
Почему именно такой подход
Роб Пайк и команда Go выбрали reference time по той же причине, по которой приняли многие другие решения в языке: читаемость важнее привычности.
Строка формата "2006-01-02 15:04:05" выступает одновременно и инструкцией, и примером результата. Глядя на неё, вы сразу видите, как будет выглядеть отформатированная дата. С "%Y-%m-%d %H:%M:%S" вам нужно мысленно декодировать каждый спецификатор.
Да, первые пять минут работы с Go-датами вызывают недоумение. Но после того как вы запомните последовательность 1-2-3-4-5-6-7, формирование любого формата становится тривиальной задачей.
Заключение
Форматирование дат в Go построено на простой идее: вместо абстрактных спецификаторов используется конкретный reference time Mon Jan 2 15:04:05 MST 2006. Каждый компонент этой даты имеет уникальный порядковый номер от 1 до 7, что делает систему логичной и запоминаемой.
На практике достаточно помнить три вещи:
- Последовательность 1-2-3-4-5-6-7 (месяц, день, час, минуты, секунды, год, часовой пояс)
- Метод
Formatдля преобразованияtime.Timeв строку - Функцию
Parse(илиParseInLocation) для обратного преобразования
Для повседневных задач в Go 1.20+ есть готовые константы time.DateOnly, time.TimeOnly и time.DateTime, которые покрывают большинство случаев.
FAQ
Почему именно 2006 год, а не любой другой?
01/02 03:04:05PM '06 -0700 каждый компонент reference time получает уникальный порядковый номер от 1 до 7. Это единственная дата, в которой числа 1, 2, 3, 4, 5, 6, 7 появляются в порядке возрастания при записи в стандартном формате даты и времени. Именно этот порядок делает мнемонику рабочей.Можно ли форматировать дату на русском языке?
time поддерживает только английские названия месяцев и дней недели. Для локализации можно использовать сторонние библиотеки (например, github.com/goodsign/monday) или написать простую функцию замены.Как получить Unix timestamp в Go?
time.Now().Unix() (секунды), time.Now().UnixMilli() (миллисекунды) или time.Now().UnixNano() (наносекунды). Для обратного преобразования воспользуйтесь time.Unix(sec, nsec).Чем отличается time.Parse от time.ParseInLocation?
time.Parse интерпретирует строку без явного часового пояса как UTC. time.ParseInLocation позволяет указать локацию, которая будет использована, если в строке нет информации о часовом поясе.Теги: