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 :=
str :=
t, err := time.Parse(layout, str)
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
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
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.
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
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?
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
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.
Blank Identifier Imports in Go: What `_` Means in an Import Statement
Learn what `_` means in a Go import, why blank identifier imports run package init code, and when to use them safely.
Calling Functions Across Files in the Same Go Package
Learn how Go uses packages across multiple files, why functions may appear undefined, and how to organize code correctly.