Форматирование дат и времени в Go простым языком: почему 2006-01-02

(последнее обновление от )

Каждый, кто приходит в 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

У этого подхода есть проблема: вам нужно помнить, что:

Перепутать регистр или букву легко, а ошибка проявится только в рантайме.

Идея Go: reference time

В Go вместо символьных спецификаторов используется конкретная дата-пример, называемая reference time (с англ. “исходное время”):

Mon Jan 2 15:04:05 MST 2006

Или в числовом виде:

01/02 03:04:05PM '06 -0700

Каждый компонент этой даты имеет уникальное числовое значение:

КомпонентЗначениеЧто обозначает
Месяц01Январь (1-й месяц)
День022-е число
Час03 или 153 часа дня: 03 для 12-часового, 15 для 24-часового формата
Минуты044 минуты
Секунды055 секунд
Год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

Час

ШаблонРезультатПример
1524-часовой формат14
0312-часовой с ведущим нулём02
312-часовой без ведущего нуля2
PMМаркер AM/PM (верхний регистр)PM
pmМаркер am/pm (нижний регистр)pm

Минуты и секунды

ШаблонРезультатПример
04Минуты с ведущим нулём05
4Минуты без ведущего нуля5
05Секунды с ведущим нулём09
5Секунды без ведущего нуля9

Доли секунды

Шаблоны с нулями (0) всегда выводят указанное количество знаков, дополняя нулями справа. Шаблоны с девятками (9) отбрасывают незначащие нули в конце:

ШаблонРезультатПример (для .123456789)
.0003 знака (с нулями).123
.0000006 знаков (с нулями).123456
.0000000009 знаков (с нулями).123456789
.91 знак (без trailing zeros).1
.992 знака (без trailing zeros).12
.9993 знака (без trailing zeros).123

Часовой пояс

Форматы с префиксом Z выводят букву Z для UTC и числовое смещение для остальных зон. Форматы с - всегда выводят числовое смещение:

ШаблонРезультатПример (MSK)Пример (UTC)
MSTАббревиатураMSKUTC
Z07:00Z или ±hh:mm+03:00Z
Z0700Z или ±hhmm+0300Z
Z07Z или ±hh+03Z
-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, эта таблица поможет быстро найти аналог:

strftimeGoОписание
%Y2006Год (4 цифры)
%y06Год (2 цифры)
%m01Месяц (01-12)
%d02День (01-31)
%H15Час (00-23)
%I03Час (01-12)
%M04Минуты (00-59)
%S05Секунды (00-59)
%pPMAM/PM
%ZMSTЧасовой пояс
%z-0700Смещение UTC
%AMondayДень недели
%aMonДень недели (сокр.)
%BJanuaryМесяц (полное)
%bJanМесяц (сокр.)

Почему именно такой подход

Роб Пайк и команда 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. Последовательность 1-2-3-4-5-6-7 (месяц, день, час, минуты, секунды, год, часовой пояс)
  2. Метод Format для преобразования time.Time в строку
  3. Функцию 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 позволяет указать локацию, которая будет использована, если в строке нет информации о часовом поясе.

Теги: