Skip to content
gohacks.dev
Basics

Formatting Time in Go and the Strange Reference Date

A lookup table for Go's time layout strings, built on the reference date 01/02 03:04:05PM 2006 -0700, with real output for each and the mistakes that bite.

6 min read
Summarize in AI
Flat illustration of an editor window full of code.

Go does not use %Y-%m-%d or YYYY-MM-DD for formatting dates. It asks you to write an example of the date you want, using one fixed moment, and it formats or parses by matching your example against it. That is a strange idea until the mnemonic clicks, and then it is difficult to forget. This is the reference I use for it: the table first, real output for each layout, the constants, and the mistakes that produce wrong output without an error. It is a companion to time and durations in Go, which covers the arithmetic, and it is part of the Go basics path.

Tested with Go 1.25.6, macOS on Apple Silicon (arm64)

The layout elements

The reference moment is Mon Jan 2 15:04:05 MST 2006, and it is also written 01/02 03:04:05PM '06 -0700. Read the second form left to right and the numbers count from 1 to 7: month 1, day 2, hour 3, minute 4, second 5, year 6, zone 7.

To getWriteNotes
Year, four digits2006
Year, two digits06
Month, number1 or 0101 is zero padded
Month, nameJan or January
Day of month2, 02 or _2_2 pads with a space
Day of year002
WeekdayMon or Monday
Hour, 24 hour15
Hour, 12 hour3 or 03Pair with PM
Minute4 or 04
Second5 or 05
AM or PMPM or pm
Fraction, fixed.000, .000000, .000000000Digits as written
Fraction, trimmed.999, .999999999Drops trailing zeros
Zone offset-0700, -07:00, -07
Zone offset or ZZ0700, Z07:00Prints Z for UTC
Zone abbreviationMSTUTC, EST, and so on

The same moment in thirteen layouts

This program formats one instant, Saturday March 9, 2024 at 14:05:07.123456789 UTC, with a range of layouts, including two that are wrong:

main.goGo
package main

import (
	"fmt"
	"time"
)

func main() {
	t := time.Date(2024, time.March, 9, 14, 5, 7, 123456789, time.UTC)

	layouts := []string{
		"YYYY-MM-DD",
		"yyyy-mm-dd HH:MM:SS",
		"2024-03-09",
		"2006-1-2",
		"02/01/06",
		"3:04PM",
		"15:04:05.000",
		"15:04:05.999999",
		"Mon Jan _2",
		"Monday, 2 January 2006",
		"2006-01-02T15:04:05Z07:00",
		"Z0700 -07:00 MST",
		"20060102150405",
	}
	for _, l := range layouts {
		fmt.Printf("%-30q %s\n", l, t.Format(l))
	}
}
Text
"YYYY-MM-DD"                   YYYY-MM-DD
"yyyy-mm-dd HH:MM:SS"          yyyy-mm-dd HH:MM:SS
"2024-03-09"                   9095-02-09
"2006-1-2"                     2024-3-9
"02/01/06"                     09/03/24
"3:04PM"                       2:05PM
"15:04:05.000"                 14:05:07.123
"15:04:05.999999"              14:05:07.123456
"Mon Jan _2"                   Sat Mar  9
"Monday, 2 January 2006"       Saturday, 9 March 2024
"2006-01-02T15:04:05Z07:00"    2024-03-09T14:05:07Z
"Z0700 -07:00 MST"             Z +00:00 UTC
"20060102150405"               20240309140507

The right-hand column is the output, and the %-30q in the print statement is a quoted, left-aligned string verb from the fmt verbs reference. Read the first two rows as warnings. YYYY-MM-DD and yyyy-mm-dd HH:MM:SS came out unchanged, because none of those letters is part of Go's vocabulary. No error is reported, so the symptom is a log line or a file name that contains the literal text YYYY. The third row is the sneakiest: someone wrote an example date, 2024-03-09, as the layout, and got 9095-02-09. Go replaced the fragments it recognized inside that text and produced nonsense, which is why a real date can never be a layout. The layout must be built from the reference moment's digits, 2006-01-02, and never from a real date.

The remaining rows show the real behavior. 3:04PM printed 2:05PM, a 12 hour clock without zero padding, and 15:04:05.000 shows three fixed digits of fraction while .999999 shows six and would drop trailing zeros. _2 pads a single-digit day with a space, which is the format ls uses and RFC 822 dates need. The zone elements show Z for UTC in the Z07:00 form and +00:00 in -07:00. The compact form 20060102150405 is a common file or ID timestamp.

The standard layouts, by name

The time package defines constants for the formats you will see most. Printing them shows what they are:

main.goGo
package main

import (
	"fmt"
	"time"
)

func main() {
	t := time.Date(2024, time.March, 9, 14, 5, 7, 0, time.UTC)

	names := []struct {
		name   string
		layout string
	}{
		{"DateTime", time.DateTime},
		{"DateOnly", time.DateOnly},
		{"TimeOnly", time.TimeOnly},
		{"Kitchen", time.Kitchen},
		{"RFC822", time.RFC822},
		{"RFC1123", time.RFC1123},
		{"RFC1123Z", time.RFC1123Z},
		{"RFC3339", time.RFC3339},
		{"RFC3339Nano", time.RFC3339Nano},
		{"StampMilli", time.StampMilli},
	}
	for _, n := range names {
		fmt.Printf("%-12s %-36q %s\n", n.name, n.layout, t.Format(n.layout))
	}
}
Text
DateTime     "2006-01-02 15:04:05"                2024-03-09 14:05:07
DateOnly     "2006-01-02"                         2024-03-09
TimeOnly     "15:04:05"                           14:05:07
Kitchen      "3:04PM"                             2:05PM
RFC822       "02 Jan 06 15:04 MST"                09 Mar 24 14:05 UTC
RFC1123      "Mon, 02 Jan 2006 15:04:05 MST"      Sat, 09 Mar 2024 14:05:07 UTC
RFC1123Z     "Mon, 02 Jan 2006 15:04:05 -0700"    Sat, 09 Mar 2024 14:05:07 +0000
RFC3339      "2006-01-02T15:04:05Z07:00"          2024-03-09T14:05:07Z
RFC3339Nano  "2006-01-02T15:04:05.999999999Z07:00" 2024-03-09T14:05:07Z
StampMilli   "Jan _2 15:04:05.000"                Mar  9 14:05:07.000

For anything that leaves your process, use time.RFC3339. It is unambiguous about the zone, sorts correctly as a string within one zone and is what JSON, HTTP APIs and most databases expect, and time.Time already marshals itself that way in JSON. RFC1123 and RFC1123Z are the formats of HTTP headers and email, and http.TimeFormat is a fixed-GMT variant for the Date header. DateTime, DateOnly and TimeOnly arrived in Go 1.20 and replace the layouts people used to copy from each other. Kitchen is a friendly 3:04PM for display only.

Parsing uses the same layouts

time.Parse(layout, value) reads a string that matches the layout, and it is strict about it. A missing zero, a wrong separator or a month out of range returns an error that quotes the layout and the value, so a mismatch is easy to read. The part that surprises people is the zone. A string with no zone is parsed as UTC, whatever the machine's local time is, and time.ParseInLocation is the call that attaches a specific location. Parsing Jan 2 with no year gives year 0, which is rarely a date you want. For configuration, the easiest approach is to accept RFC 3339 only, and to document that choice. When the text is typed by a person, as in a program that reads input from the terminal, print the expected layout in the prompt, since a user cannot guess it.

The mistakes, collected

SymptomCauseFix
Output contains literal YYYY or mmLayout written from another languageUse 2006 and 01
Date prints as 9095-02-09A real date used as the layoutUse the reference date 2006-01-02
Minutes show the month01 where 04 was meant01 is month, 04 is minute
Hour is off by 1203 without PM, or 15 and PM togetherUse 15 alone or 03 with PM
Fraction has trailing zeros.000Use .999
Parse fails on single digit dayLayout 02 wants two digitsUse 2 to accept either
Parsed time is in UTCNo zone in the stringUse ParseInLocation or include the offset
Z07:00 prints +00:00Used -07:00Z07:00 prints Z for UTC

What is not here

The layout language has no elements for week numbers, quarters or era, no %U or %j style extras beyond day of year, and no locale: month and day names are always English. If you need Samedi 9 mars, format the numbers and look up names yourself, or use golang.org/x/text's date formatting support when it fits. ISO week numbers are available from t.ISOWeek() and you format them by hand. If you generate formats for other systems, check which convention they use, since many databases and C libraries use strftime codes, and the date format converter maps between them. The authoritative text is the constant list and the long comment in the time package documentation, and the parsing and formatting code is in time/format.go.

Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.