Question
I am trying to parse the date string "2014-09-12T11:45:26.371Z" in Go. This format is commonly described as an RFC 3339 or ISO 8601 date-time.
Here is the code:
layout := "2014-09-12T11:45:26.371Z"
str := "2014-11-12T11:45:26.371Z"
t, err := time.Parse(layout, str)
This produces an error like:
parsing time "2014-11-12T11:47:39.489Z": month out of range
How should this date string be parsed correctly in Go?
Short Answer
By the end of this page, you will understand how Go parses date-time strings, why your layout string is incorrect, and how to correctly parse RFC3339 and ISO 8601 timestamps using time.Parse, time.RFC3339, and time.RFC3339Nano.
Concept
Go handles date parsing differently from many other languages. Instead of using symbolic tokens like YYYY-MM-DD, Go uses a reference date to define layouts.
The reference date is:
Mon Jan 2 15:04:05 MST 2006
To parse a date string, you must write the layout using the exact shape of that reference date in the format you expect.
For example, this is a correct RFC3339-style layout with milliseconds and UTC:
2006-01-02T15:04:05.000Z
Or, more commonly, you can use Go's built-in constant:
time.RFC3339
or
time.RFC3339Nano
Why your code failed:
layout := "2014-09-12T11:45:26.371Z"
This is not a layout in Go's format system. It is just a specific date. Go tries to interpret pieces of it as layout markers based on the reference date rules, which leads to incorrect parsing and errors like month out of range.
This matters in real programming because APIs, logs, databases, and JSON payloads often store timestamps in RFC3339 or ISO 8601 format. If you parse them incorrectly, your app may reject valid input, store wrong times, or mis-handle time zones.
Mental Model
Think of Go's time layout as a stencil made from one special example date.
You do not write:
YYYYfor yearMMfor monthDDfor day
Instead, you reshape this fixed sample date:
2006= year01= month02= day15= hour04= minute05= second
So parsing in Go is like saying:
"Here is a sample timestamp pattern. Read my input if it matches this exact pattern."
If the stencil is wrong, Go reads the input in the wrong places and parsing fails.
Syntax and Examples
Core syntax
t, err := time.Parse(layout, input)
layoutdescribes the expected format using Go's reference dateinputis the string you want to parse
Correct way to parse RFC3339
package main
import (
"fmt"
"time"
)
func main() {
str := "2014-11-12T11:45:26.371Z"
t, err := time.Parse(time.RFC3339Nano, str)
if err != nil {
fmt.Println("parse error:", err)
return
}
fmt.Println(t)
}
Why time.RFC3339Nano?
The input contains fractional seconds:
.371
time.RFC3339Nano handles RFC3339 timestamps with optional fractional seconds.
Using a custom layout
You can also write the layout yourself:
layout := "2006-01-02T15:04:05.000Z"
str := "2014-11-12T11:45:26.371Z"
t, err := time.Parse(layout, str)
This works because the layout uses Go's reference date structure.
Parsing a timestamp with timezone offset
str := "2014-11-12T11:45:26+02:00"
t, err := time.Parse(time.RFC3339, str)
This also works because RFC3339 allows offsets like +02:00.
Important note
RFC3339 is a profile of ISO 8601. In practice, many Go programs parse API timestamps with time.RFC3339 or time.RFC3339Nano.
Step by Step Execution
Consider this code:
package main
import (
"fmt"
"time"
)
func main() {
str := "2014-11-12T11:45:26.371Z"
t, err := time.Parse(time.RFC3339Nano, str)
fmt.Println(t)
fmt.Println(err)
}
Step by step
-
strstores the input timestamp:2014-11-12T11:45:26.371Z -
time.Parse(time.RFC3339Nano, str)tells Go:- expect an RFC3339 date-time
- allow fractional seconds
- allow timezone information such as
Z
-
Go reads the pieces:
2014→ year11→ month12→ day11→ hour45→ minute26→ second.371→ fractional secondZ→ UTC timezone
-
If the string matches the expected layout, Go returns a
time.Timevalue. -
errbecomesnil, so parsing succeeded. -
Printing
tshows the parsed time, usually in a readable Go time format.
What went wrong in the original code?
layout := "2014-09-12T11:45:26.371Z"
Go does not treat this as a generic pattern. It treats it as a layout based on the reference date rules. Since 2014 is not the special year marker 2006, and 09 is not the month marker 01, the parser cannot reliably match the input and ends up producing confusing errors.
Real World Use Cases
APIs
Most REST APIs return timestamps like:
{"created_at": "2024-01-15T09:30:00Z"}
Go services often parse these using time.RFC3339.
JSON payloads
When receiving request bodies with date fields, you may need to validate and parse timestamps before storing them.
Logs and monitoring
Structured logs often use RFC3339 because it is readable, sortable, and timezone-aware.
Database import/export
CSV files, migrations, and exports frequently include ISO-like timestamps that must be parsed consistently.
Scheduling systems
Job runners, reminder apps, and calendar systems depend on correct parsing of time zones and UTC timestamps.
Real Codebase Usage
In real Go codebases, developers rarely hardcode example dates as layouts. Instead, they usually:
Use built-in constants
t, err := time.Parse(time.RFC3339, input)
or
t, err := time.Parse(time.RFC3339Nano, input)
This is clearer and less error-prone.
Validate early
A common pattern is to reject bad timestamps at the edge of the system:
func parseCreatedAt(input string) (time.Time, error) {
if input == "" {
return time.Time{}, fmt.Errorf("created_at is required")
}
t, err := time.Parse(time.RFC3339Nano, input)
if err != nil {
return time.Time{}, fmt.Errorf("invalid created_at: %w", err)
}
return t, nil
}
Use guard clauses
Short early returns keep parsing logic simple.
Normalize to UTC
Many systems convert parsed times to UTC before storing them:
utc := t.UTC()
Wrap errors with context
This helps debugging when timestamps come from external systems.
Parse at boundaries
A common design is:
- parse strings when reading API input
- keep values as
time.Timeinside the application - format strings only when returning output
Common Mistakes
1. Using a sample date instead of Go's reference date
Broken code:
layout := "2014-09-12T11:45:26.371Z"
Correct code:
layout := "2006-01-02T15:04:05.000Z"
Or better:
layout := time.RFC3339Nano
2. Assuming Go uses YYYY-MM-DD
Broken expectation:
layout := "YYYY-MM-DDTHH:mm:ssZ"
Go does not support these symbolic tokens.
3. Using time.RFC3339 when fractional seconds need flexibility
If your input may contain fractional seconds, time.RFC3339Nano is often the safest choice.
t, err := time.Parse(time.RFC3339Nano, "2014-11-12T11:45:26.371Z")
4. Ignoring timezone information
A timestamp ending in Z means UTC.
If you strip timezone data or use the wrong layout, you may parse the time incorrectly.
5. Ignoring errors
Broken code:
t, _ := time.Parse(time.RFC3339Nano, str)
Always check err so invalid input does not silently become bad data.
6. Expecting all ISO 8601 variants to parse with one layout
ISO 8601 allows many forms. Your input must match the layout you provide.
Examples that may require different handling:
2024-01-15T10:30:00Z2024-01-15T10:30:00.123Z2024-01-15T10:30:00+02:0020240115T103000Z
Comparisons
| Option | Example | Best for | Notes |
|---|---|---|---|
time.RFC3339 | 2024-01-15T10:30:00Z | Standard RFC3339 timestamps | Good default for most API timestamps |
time.RFC3339Nano | 2024-01-15T10:30:00.123456789Z | RFC3339 with optional fractional seconds | More flexible when precision varies |
| Custom layout | 2006-01-02T15:04:05.000Z | Non-standard or exact fixed formats | Useful when input format is known and strict |
time.RFC3339 vs time.RFC3339Nano
| Feature | time.RFC3339 | time.RFC3339Nano |
|---|---|---|
| Supports timezone offsets | Yes | Yes |
Supports Z UTC marker | Yes | Yes |
| Supports fractional seconds | Limited for exact formatting needs | Better for variable fractional precision |
| Common use | Standard API timestamps | API timestamps with milliseconds or nanoseconds |
Built-in constant vs custom layout
| Choice | Pros | Cons |
|---|---|---|
| Built-in constant | Readable, standard, less error-prone | Only fits standard formats |
| Custom layout | Precise control | Easy to get wrong if you forget Go's reference date |
Cheat Sheet
// Parse standard RFC3339
t, err := time.Parse(time.RFC3339, input)
// Parse RFC3339 with optional fractional seconds
t, err := time.Parse(time.RFC3339Nano, input)
// Custom layout example
layout := "2006-01-02T15:04:05.000Z"
t, err := time.Parse(layout, input)
Go reference date pieces
2006 = year
01 = month
02 = day
15 = hour (24-hour)
04 = minute
05 = second
.000 = milliseconds
Z07:00 = timezone offset
Useful built-in layouts
time.RFC3339
time.RFC3339Nano
Rules to remember
- Go does not use
YYYY-MM-DD - Layouts must be written using the reference date
- Always check parsing errors
- Use
time.RFC3339Nanowhen fractional seconds may appear Zmeans UTC
Common working examples
time.Parse(time.RFC3339, "2024-01-15T10:30:00Z")
time.Parse(time.RFC3339Nano, "2024-01-15T10:30:00.123Z")
time.Parse(time.RFC3339, "2024-01-15T10:30:00+02:00")
FAQ
Why does Go use 2006-01-02 instead of YYYY-MM-DD?
Go's time package uses a specific reference date as the layout model. You must rewrite that exact date into the shape you want.
Should I use time.RFC3339 or time.RFC3339Nano?
Use time.RFC3339 for standard timestamps and time.RFC3339Nano when fractional seconds may be present.
What does the Z at the end of the timestamp mean?
Z means UTC, also called Zulu time.
Is RFC3339 the same as ISO 8601?
RFC3339 is a narrower, more specific profile of ISO 8601. Many API timestamps described as ISO 8601 are effectively RFC3339-compatible.
Why did I get month out of range?
Because the layout string was written as a sample date instead of using Go's reference date format, so the parser interpreted parts incorrectly.
Can I parse timestamps with offsets like +05:30?
Yes. time.RFC3339 and time.RFC3339Nano support timezone offsets.
Can I store parsed times as strings?
You can, but inside your Go program it is usually better to convert them to time.Time and only format them as strings when needed.
Mini Project
Description
Build a small Go program that reads a list of timestamp strings, parses them safely, and prints either the normalized UTC time or an error. This demonstrates correct RFC3339 parsing, error handling, and working with time.Time values in a practical way.
Goal
Create a parser that accepts RFC3339-style timestamps, handles fractional seconds, and reports valid and invalid inputs clearly.
Requirements
- Read several timestamp strings from a slice
- Parse each timestamp using Go's time package
- Print the parsed time in UTC when valid
- Print a helpful error message when parsing fails
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.