iTranslated by AI
Should I Use ISO 8601 or RFC 3339 in Go?
Introduction
When deciding on a date-time format for APIs in Go, it can be confusing to choose between ISO 8601 and RFC 3339.
I have actually received code review feedback stating, "You wrote ISO 8601, but I'm not sure which specific representation you intend." Because ISO 8601 covers a wide range of formats, simply writing "ISO 8601 format" in a specification can lead to discrepancies in interpretation by the implementation team.
In conclusion, it is safest to use RFC 3339 as the standard in Go.
This article outlines the differences between the two and how to use them effectively in Go.
The Relationship between ISO 8601 and RFC 3339
First, as a premise, RFC 3339 does not completely replace ISO 8601.
- ISO 8601 is a broader standard with many variations for expressing date and time.
- RFC 3339 is based on ISO 8601 but specifies a narrower subset tailored for date-time expressions in internet protocols.
ISO 8601 establishes a large framework for "expressing dates and times according to certain rules," but it has many variations, and the choice of which format to use is often left to the implementer. For example, both 2026-03-20T10:00:00Z and 20260320T100000Z are valid ISO 8601.
RFC 3339 restricts these variations to "use cases for internet protocols," which makes it much easier for different implementations to interpret data consistently. It is characterized by being easy to implement and simple to write parsers for.
Simply stating "it supports ISO 8601" is prone to ambiguity in implementation, which can lead to misaligned expectations in specifications and code reviews.
Why Go Recommends RFC 3339
The time package in Go provides time.RFC3339 and time.RFC3339Nano as standard constants, and they are widely used as the de facto standard formats for handling dates and times in APIs and JSON.
t := time.Now().UTC()
s := t.Format(time.RFC3339)
// Example: "2026-03-20T10:00:00Z"
s := t.Format(time.RFC3339Nano)
// Example: "2026-03-20T10:00:00.123456789Z"
Both formatting and parsing can be handled entirely within the standard library.
Usage Guidelines
| Context | Format to Use |
|---|---|
| API Response / JSON | RFC 3339 |
| External Service Integration | RFC 3339 |
Internal systems requiring explicit YYYY-MM-DD
|
Custom format |
| When you want to advertise "ISO 8601 support" | Unify implementation with RFC 3339 |
It is common to use RFC 3339 in implementation while claiming to "comply with ISO 8601," and this is not a problem in practice. Since RFC 3339 is a restricted subset of ISO 8601, it is treated as a valid expression within the scope of ISO 8601 for general purposes.
Is there a need to use ISO 8601 directly in Go?
In conclusion, almost never.
The main cases where you might focus on ISO 8601 in Go are:
- The external specification or requirements document explicitly states "must be ISO 8601 format."
- You need expressions not handled by RFC 3339, such as date only (
2026-03-20) or week representation (2026-W12). - You need to parse input that allows multiple date-time formats.
When handling only dates, you can use time.DateOnly (available since Go 1.20).
s := t.Format(time.DateOnly)
// Example: "2026-03-20"
For typical API development that handles date-times with time zones, you can standardize on RFC 3339 without needing to worry about ISO 8601 directly.
How to Write in Swagger (OpenAPI) Specifications
In OpenAPI (Swagger), there are several options for the format of a date-time field.
| format | Meaning | Example |
|---|---|---|
date-time |
RFC 3339 format date-time | 2026-03-20T10:00:00Z |
date |
Date only (ISO 8601) | 2026-03-20 |
It is standard to use date-time for date-time fields in public APIs.
createdAt:
type: string
format: date-time
example: "2026-03-20T10:00:00Z"
format: date-time signifies RFC 3339. Since this is defined in the OpenAPI specification, writing date-time conveys your intent more accurately to the reader than writing "ISO 8601."
For date-only fields, use date.
birthDate:
type: string
format: date
example: "2026-03-20"
Explicitly specifying the time zone handling in Swagger can also prevent misalignments between the front-end and back-end.
createdAt:
type: string
format: date-time
description: Returns UTC time in RFC 3339 format
example: "2026-03-20T10:00:00Z"
Points to Note
RFC 3339Nano omits trailing zeros for nanoseconds
time.RFC3339Nano omits trailing zeros, so the length of the string varies.
// Trailing zeros are omitted
"2026-03-20T10:00:00.1Z" // 100ms
"2026-03-20T10:00:00.123Z" // 123ms
"2026-03-20T10:00:00.123456Z" // 123456µs
If you save these as strings and want to sort them alphabetically, they may not align chronologically because the number of digits is not constant. You should be careful if you have sorting requirements.
The Go parser is not strictly compliant
time.Parse(time.RFC3339, s) is not strictly compliant with the RFC specification in some aspects. For example, it may fail to handle leap seconds correctly (represented as 60 for seconds), which are permitted by the RFC, or it may accept expressions that the RFC should reject.
If you are accepting input from external sources, consider adding separate validation logic for the parsed value.
Summary
When deciding on date-time formats for APIs in Go, using RFC 3339 as your standard will lead to more consistency.
- ISO 8601 is a broad standard that is prone to variations in interpretation.
- RFC 3339 is a subset of ISO 8601 and is the format best suited for Go and APIs.
- You don't need external libraries since
time.RFC3339/time.RFC3339Nanoare available as standard. - Be careful with omitted trailing zeros when using
RFC3339Nanofor storage or sorting. - There is almost no need to consciously use ISO 8601 directly in Go; standardizing on RFC 3339 is sufficient.
- In Swagger, use
format: date-timefor timestamp fields to explicitly indicate RFC 3339.
Discussion