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.

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 get | Write | Notes |
|---|---|---|
| Year, four digits | 2006 | |
| Year, two digits | 06 | |
| Month, number | 1 or 01 | 01 is zero padded |
| Month, name | Jan or January | |
| Day of month | 2, 02 or _2 | _2 pads with a space |
| Day of year | 002 | |
| Weekday | Mon or Monday | |
| Hour, 24 hour | 15 | |
| Hour, 12 hour | 3 or 03 | Pair with PM |
| Minute | 4 or 04 | |
| Second | 5 or 05 | |
| AM or PM | PM or pm | |
| Fraction, fixed | .000, .000000, .000000000 | Digits as written |
| Fraction, trimmed | .999, .999999999 | Drops trailing zeros |
| Zone offset | -0700, -07:00, -07 | |
Zone offset or Z | Z0700, Z07:00 | Prints Z for UTC |
| Zone abbreviation | MST | UTC, 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:
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))
}
}"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" 20240309140507The 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:
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))
}
}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.000For 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
| Symptom | Cause | Fix |
|---|---|---|
Output contains literal YYYY or mm | Layout written from another language | Use 2006 and 01 |
Date prints as 9095-02-09 | A real date used as the layout | Use the reference date 2006-01-02 |
| Minutes show the month | 01 where 04 was meant | 01 is month, 04 is minute |
| Hour is off by 12 | 03 without PM, or 15 and PM together | Use 15 alone or 03 with PM |
| Fraction has trailing zeros | .000 | Use .999 |
| Parse fails on single digit day | Layout 02 wants two digits | Use 2 to accept either |
| Parsed time is in UTC | No zone in the string | Use ParseInLocation or include the offset |
Z07:00 prints +00:00 | Used -07:00 | Z07: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.


