Question

How to Parse RFC3339 / ISO 8601 Date-Time Strings in Go

godate

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:

  • YYYY for year
  • MM for month
  • DD for day

Instead, you reshape this fixed sample date:

  • 2006 = year
  • 01 = month
  • 02 = day
  • 15 = hour
  • 04 = minute
  • 05 = 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.

Take Quiz

Syntax and Examples

Core syntax

t, err := time.Parse(layout, input)
  • layout describes the expected format using Go's reference date
  • input is 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

  1. str stores the input timestamp:

    2014-11-12T11:45:26.371Z
    
  2. time.Parse(time.RFC3339Nano, str) tells Go:

    • expect an RFC3339 date-time
    • allow fractional seconds
    • allow timezone information such as Z
  3. Go reads the pieces:

    • 2014 → year
    • 11 → month
    • 12 → day
    • 11 → hour
    • 45 → minute
    • 26 → second
    • .371 → fractional second
    • Z → UTC timezone
  4. If the string matches the expected layout, Go returns a time.Time value.

  5. err becomes nil, so parsing succeeded.

  6. Printing t shows 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.

Take Quiz

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.Time inside 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:00Z
  • 2024-01-15T10:30:00.123Z
  • 2024-01-15T10:30:00+02:00
  • 20240115T103000Z

Comparisons

OptionExampleBest forNotes
time.RFC33392024-01-15T10:30:00ZStandard RFC3339 timestampsGood default for most API timestamps
time.RFC3339Nano2024-01-15T10:30:00.123456789ZRFC3339 with optional fractional secondsMore flexible when precision varies
Custom layout2006-01-02T15:04:05.000ZNon-standard or exact fixed formatsUseful when input format is known and strict

time.RFC3339 vs time.RFC3339Nano

Featuretime.RFC3339time.RFC3339Nano
Supports timezone offsetsYesYes
Supports Z UTC markerYesYes
Supports fractional secondsLimited for exact formatting needsBetter for variable fractional precision
Common useStandard API timestampsAPI timestamps with milliseconds or nanoseconds

Built-in constant vs custom layout

ChoiceProsCons
Built-in constantReadable, standard, less error-proneOnly fits standard formats
Custom layoutPrecise controlEasy to get wrong if you forget Go's reference date
Take Quiz

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.RFC3339Nano when fractional seconds may appear
  • Z means 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.

Take Quiz

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
Take Quiz

Keep learning

Related questions

View more