Parsing RFC 3339 / ISO-8601 in Go: Why Even Experienced Developers Trip Up
\n\nHave you ever written something like time.Parse("2006-01-02T15:04:05Z", input) in Go, got an error, and felt like the language was deliberately mocking you? Sounds familiar. Parsing dates and times is one of those topics where everything seems simple, but in practice, many pitfalls await.
On Stack Overflow, the question "Parsing RFC-3339 / ISO-8601 date-time string in Go" has gathered thousands of views. People get confused with formats, forget about nanoseconds, struggle with time zones, and get frustrated by the "magic" number 2006-01-02T15:04:05Z. In this article, we'll break down the 5 most common errors and show how to avoid them once and for all. Let's go!
A Bit of Theory: What Are RFC 3339 and ISO 8601?
\n\nBefore diving into the code, let's quickly clarify the terms. ISO 8601 is an international standard for representing dates and times. RFC 3339 is a "stripped-down" version of it, popular on the web (e.g., in JSON, APIs, logs).
\n\nTypical RFC 3339 strings look like this:
\n\n2025-01-15T10:30:00Z\n2025-01-15T10:30:00+03:00\n2025-01-15T10:30:00.123456789Z\n\nIn Go, the time.Parse(layout, value) function is used for parsing. And this is where the fun begins.
Error #1: Incorrect Layout — The Main Cause of Headaches
\n\nGo uses a unique approach: the format is specified not with codes like YYYY-MM-DD, but with a specific date: Mon Jan 2 15:04:05 MST 2006. It's easy to remember: 01/02 03:04:05PM '06 -0700 (month, day, hour, minute, second, year, time zone).
Beginners often try to write something like:
\n\n// ❌ Error!\nt, err := time.Parse("YYYY-MM-DDTHH:MM:SSZ", "2025-01-15T10:30:00Z")\n\nGo simply won't understand this format. The correct layout for RFC 3339 is time.RFC3339:
// ✅ Correct\nt, err := time.Parse(time.RFC3339, "2025-01-15T10:30:00Z")\nif err != nil {\n log.Fatal(err)\n}\nfmt.Println(t) // 2025-01-15 10:30:00 +0000 UTC\n\nThe constant time.RFC3339 equals the string "2006-01-02T15:04:05Z07:00". Note the Z07:00 — this is the time zone notation.
Error #2: Ignoring Nanoseconds (time.RFC3339 vs time.RFC3339Nano)
\n\nThe standard time.RFC3339 does not support nanoseconds. If your string contains a fractional second:
2025-01-15T10:30:00.123456789Z\n\n…then parsing with time.RFC3339 will return an error:
// ❌ Error: parsing time "2025-01-15T10:30:00.123456789Z" as "2006-01-02T15:04:05Z07:00": cannot parse ".123456789Z" as "Z07:00"\nt, err := time.Parse(time.RFC3339, "2025-01-15T10:30:00.123456789Z")\n\nThe solution is to use time.RFC3339Nano:
// ✅ Correct\nt, err := time.Parse(time.RFC3339Nano, "2025-01-15T10:30:00.123456789Z")\nif err != nil {\n log.Fatal(err)\n}\nfmt.Println(t) // 2025-01-15 10:30:00.123456789 +0000 UTC\n\nTip: If you're unsure whether nanoseconds will be present, use time.RFC3339Nano — it's backward compatible and will work for both cases.
Error #3: Time Zone — Is Z or +00:00 the Same Thing?
\n\nThe strings 2025-01-15T10:30:00Z and 2025-01-15T10:30:00+00:00 mean the same thing (UTC), but Go may handle them differently if the layout is specified incorrectly.
// ✅ Both variants work with time.RFC3339\nt1, _ := time.Parse(time.RFC3339, "2025-01-15T10:30:00Z")\nt2, _ := time.Parse(time.RFC3339, "2025-01-15T10:30:00+00:00")\nfmt.Println(t1.Equal(t2)) // true\n\nBut if you write your own layout, be careful:
\n\n- \