🕰️ Analogy
The time package gives you three instruments in one: a calendar (a time.Time instant — this date, this clock reading, this zone), a ruler for spans (time.Duration — how long between two instants), and a stopwatch (time.Now + time.Since). The famous oddity is how you tell it a date format — not with %Y%m%d codes, but by spelling out one specific reference moment.
Time and Duration
A time.Time is an instant in time; a time.Duration is a span measured in nanoseconds, with friendly named constants (time.Second, time.Hour). Because a Duration is just an int64, you do arithmetic on it directly:
package main
import (
"fmt"
"time"
)
func main() {
// a fixed instant keeps this output deterministic
t := time.Date(2026, time.June, 6, 15, 4, 5, 0, time.UTC)
fmt.Println("RFC3339:", t.Format(time.RFC3339)) // 2026-06-06T15:04:05Z
fmt.Println("custom: ", t.Format("2006-01-02 15:04")) // 2026-06-06 15:04
// component accessors
fmt.Println("year:", t.Year(), "month:", t.Month(), "day:", t.Day())
fmt.Println("weekday:", t.Weekday()) // Saturday
// Duration is int64 nanoseconds — arithmetic just works
d := 90 * time.Minute
fmt.Println("hours: ", d.Hours()) // 1.5
fmt.Println("later: ", t.Add(d).Format("15:04")) // 16:34
fmt.Println("until: ", t.Add(24*time.Hour).Sub(t)) // 24h0m0s
}
The reference layout
This is the part everyone has to learn once. Rather than format codes, Go uses one reference instant and asks you to write it the way you want your dates to look. The reference is Mon Jan 2 15:04:05 MST 2006 — chosen because its components are the sequence 1 2 3 4 5 6 7 (month=1, day=2, hour=3pm=15, minute=4, second=5, year=06, zone=−07):
graph LR REF["Mon Jan 2 15:04:05 MST 2006"] --> N["01 / 02 03:04:05 PM 2006 -0700"] N --> SEQ["the sequence 1 2 3 4 5 6 7 — memorize this one moment"]
So "2006-01-02" is an ISO date, "15:04" is a 24-hour clock, "Mon, 02 Jan 2006" is a readable date, and "3:04 PM" is a 12-hour clock. Parsing uses the same layout — you describe the input’s shape and time.Parse reads it:
package main
import (
"fmt"
"time"
)
func main() {
// Parse: describe the INPUT format with the reference numbers.
p, err := time.Parse("2006-01-02", "2025-12-25")
fmt.Println(p.Weekday(), err) // Thursday <nil>
// A wrong layout is a runtime error, not a silent zero.
_, err = time.Parse("2006-01-02", "Dec 25, 2025")
fmt.Println("parse error:", err != nil) // true
// Format the same instant several ways.
t := time.Date(2026, 1, 2, 15, 4, 5, 0, time.UTC)
for _, layout := range []string{"2006-01-02", "15:04", "Mon, 02 Jan 2006", "3:04 PM"} {
fmt.Printf("%-18s -> %s\n", layout, t.Format(layout))
}
}
🧪 Use the predefined layout constants
Don’t hand-roll common formats — time ships constants: time.RFC3339 (the API/JSON default), time.RFC1123 (HTTP headers), time.Kitchen (3:04PM), time.DateOnly and time.TimeOnly (Go 1.20+). time.RFC3339 is what JSON marshaling uses and what most web APIs expect, so prefer it for anything that crosses a wire.
Zones, the monotonic clock, and equality
A time.Time bundles three things: a wall-clock reading, a *time.Location (zone), and — for values from time.Now() — a monotonic clock reading. That last one is why elapsed-time measurement is reliable even if NTP adjusts the wall clock mid-measurement. It’s also why you should compare instants with Equal and never with ==:
package main
import (
"fmt"
"time"
)
func main() {
utc := time.Date(2026, 6, 6, 12, 0, 0, 0, time.UTC)
// Same instant, viewed in another zone.
ny, _ := time.LoadLocation("America/New_York")
there := utc.In(ny)
fmt.Println("UTC:", utc.Format("15:04 MST"))
fmt.Println("NY: ", there.Format("15:04 MST"))
// They are the SAME instant — == would say false (different Location),
// but Equal compares the moment itself.
fmt.Println("== : ", utc == there) // false
fmt.Println("Equal:", utc.Equal(there)) // true
}
The stopwatch, timers, and tickers
To measure real work, bracket it with time.Now() / time.Since(). For delayed or periodic work, use timers and tickers — but remember tickers must be stopped:
package main
import (
"fmt"
"time"
)
func main() {
// Stopwatch.
start := time.Now()
sum := 0
for i := 0; i < 1_000_000; i++ {
sum += i
}
fmt.Println("did work in under a second:", time.Since(start) < time.Second)
// Ticker: fires repeatedly. ALWAYS Stop it (deferred here).
ticker := time.NewTicker(10 * time.Millisecond)
defer ticker.Stop()
ticks := 0
for range ticker.C {
ticks++
if ticks == 3 {
break // stop after three ticks
}
}
fmt.Println("ticks:", ticks) // 3
}
The building blocks you’ll combine: time.Sleep(d) pauses the current goroutine; <-time.After(d) is a channel that fires once after d (great in a select for timeouts); time.NewTimer(d) is a one-shot you can Stop/Reset; time.NewTicker(d) fires on an interval until stopped.
When to use what
| You want to… | Use |
|---|---|
| The current instant | time.Now() |
| A specific date | time.Date(...) |
| Format for an API/JSON | t.Format(time.RFC3339) |
| Parse a known format | time.Parse(layout, s) |
| Measure elapsed time | time.Since(start) |
| Pause a goroutine | time.Sleep(d) |
Time out a select | <-time.After(d) |
| Do something periodically | time.NewTicker(d) + defer Stop() |
| Add/subtract time | t.Add(d) / t.Sub(u) |
| Compare instants | t.Equal(u) / t.Before(u) / t.After(u) |
⚠️ Stop your tickers; don't loop time.After
A time.NewTicker keeps firing — and keeps its runtime timer alive — until you call Stop(), so a forgotten ticker leaks. Calling time.After inside a hot loop is the subtler version: each iteration allocates a brand-new timer that lives until it fires, which can pile up under load. Reuse a Timer/Ticker, and pair timeouts with context (context.WithTimeout) so cancellation propagates instead of just elapsing.
See also
- encoding/json —
time.Timemarshals as RFC 3339 out of the box. - select —
time.Afteras a timeout case. - context —
context.WithTimeout/WithDeadlinebuild ontimefor cancellation. - rate limiting — tickers drive the simplest rate limiter.
Next: reading and writing the filesystem — files & os.
Related topics
Formatting and streaming — the fmt verbs you'll actually use, width/precision flags, the Stringer/Formatter hooks, and the tiny io.Reader/io.Writer interfaces (plus io.Copy, MultiWriter, TeeReader) that everything plugs into.
essentialsencoding/jsonTurning Go values into JSON and back — Marshal/Unmarshal, struct tags and omitempty, decoding into structs vs maps, streaming Encoder/Decoder, custom Marshaler/Unmarshaler, and json.RawMessage for deferred decoding.
Check your understanding
Score: 0 / 51. What is Go's reference time layout for formatting?
Instead of cryptic codes, Go uses one specific reference instant — 01/02 03:04:05PM '06 -0700 (i.e. 1 2 3 4 5 6 7). You write the output you want using those numbers, e.g. "2006-01-02".
2. What is a time.Duration under the hood?
Duration is int64 nanoseconds. That's why 90 * time.Minute works and d.Hours() returns 1.5 — it's arithmetic on a number, with named constants for readability.
3. How do you measure how long an operation took?
time.Now() captures an instant; time.Since(start) returns the Duration elapsed. Go's Time carries a monotonic clock reading so this stays correct even if the wall clock changes.
4. Why should you compare instants with t.Equal(u) instead of t == u?
A time.Time bundles wall clock, monotonic reading, and *Location. Two values can name the same instant in different zones, or one may carry a monotonic reading the other lacks, so == can be false. t.Equal(u) compares the instant itself.
5. What happens to a time.Ticker you never Stop()?
A Ticker holds a runtime timer that keeps delivering on its channel; the GC will not reclaim it while it's active. Always defer ticker.Stop(). (time.After has the same trap in a loop — each call allocates a fresh timer.)
Comments
Sign in with GitHub to join the discussion.