Skip to content

time

The time package provides two central types:

  • time.Time — an instant in time with nanosecond precision, always carrying a location (timezone).
  • time.Duration — a signed 64-bit integer representing a length of time in nanoseconds. Duration constants like time.Hour, time.Minute, time.Second, and time.Millisecond make arithmetic readable.
d := 2*time.Hour + 30*time.Minute + 15*time.Second
fmt.Println(d) // 2h30m15s

In production code you call time.Now() to get the current instant. For tests and deterministic examples, use time.Date:

t := time.Date(2024, 1, 2, 15, 4, 5, 0, time.UTC)
// year, month, day, hour, min, sec, nanosec, location

time.Month values (time.January through time.December) are typed constants; you can also pass integer literals (1–12) because they are assignable to time.Month.

time.Now() reads both the wall clock and, on most platforms, a monotonic clock reading, and stores both inside the time.Time value. The monotonic reading only ever moves forward, even if the OS clock is adjusted by NTP or a manual change — which is exactly what Since and Sub use, so measuring an elapsed duration is safe from clock jumps.

This dual reading is also why == and .Equal() can disagree on what looks like the same instant:

a := time.Now()
b := a // identical copy — a == b is true
c := a.Round(0) // Round(0) strips the monotonic reading
fmt.Println(a == b, a.Equal(b)) // true true
fmt.Println(a == c, a.Equal(c)) // false true

== compares the entire struct — wall clock, monotonic reading, and location all included — so two Time values representing the same instant can compare unequal if one has had its monotonic reading stripped (by Round, by a round-trip through Marshal/Unmarshal, or by time.Parse). Equal compares only the instant in time and ignores all of that. Always use Equal to compare two time.Time values, never ==, unless you specifically need to detect a stripped monotonic reading.

The n-times-time.Second unit bug and time.Sleep(5)

Section titled “The n-times-time.Second unit bug and time.Sleep(5)”

Duration constants (time.Second, time.Millisecond, …) exist so a plain integer is never mistaken for a duration — but two related mistakes still show up constantly in AI-generated code:

var n time.Duration = 2
d := n * time.Second // fine: n is already a Duration
fmt.Println(d) // 2s

The bug shows up when n is a plain int instead of time.Duration: n * time.Second where n int does not even compile (“mismatched types”), which is Go protecting you — but time.Sleep(n) compiles fine for any int-convertible value, and time.Sleep(5) sleeps for 5 nanoseconds, not 5 seconds, because Sleep takes a Duration and 5 is a bare nanosecond count once it loses the multiplication:

time.Sleep(5) // sleeps 5 nanoseconds — almost certainly not what was intended
time.Sleep(5 * time.Second) // sleeps 5 seconds — the constant makes the unit explicit

Reading a diff, time.Sleep (or context.WithTimeout, time.NewTimer, …) called with a bare integer literal without a time.Xxx unit attached is worth a second look every time.

Go uses a concrete reference time instead of abstract format codes like YYYY-MM-DD. The reference moment is:

Mon Jan 2 15:04:05 MST 2006

To format a time, write out the reference moment in the shape you want:

t.Format("2006-01-02") // "2024-01-02"
t.Format("15:04:05") // "15:04:05"
t.Format("02 Jan 2006 15:04 MST") // "02 Jan 2024 15:04 UTC"
t.Format(time.RFC3339) // "2024-01-02T15:04:05Z"

Go 1.20 added named layout constants for the most common shapes, so you no longer have to retype the reference moment for them: time.DateTime ("2006-01-02 15:04:05"), time.DateOnly ("2006-01-02"), and time.TimeOnly ("15:04:05").

time.Parse is the inverse of Format — it takes a layout and a string:

parsed, err := time.Parse("2006-01-02", "2024-06-15")
// parsed is midnight UTC on June 15, 2024

The layout string must match the reference time components you used — the same characters in the same positions.

time.Parse always returns a Time in UTC when the layout has no timezone component — it never assumes the machine’s local timezone. When the input string represents a local time in a specific timezone (a timestamp from a log file written in Asia/Bangkok, say), use time.ParseInLocation(layout, value, loc) instead, which interprets the string as wall-clock time in loc rather than UTC:

loc, _ := time.LoadLocation("Asia/Bangkok")
p1, _ := time.Parse("2006-01-02 15:04:05", "2026-09-13 14:30:00") // UTC
p2, _ := time.ParseInLocation("2006-01-02 15:04:05", "2026-09-13 14:30:00", loc) // Asia/Bangkok
// p1 and p2 represent different instants even though the digits are identical
deadline := t.Add(72 * time.Hour) // 3 days later
diff := deadline.Sub(t) // time.Duration: 72h0m0s
before := deadline.Before(t) // false
after := deadline.After(t) // true

Add takes a Duration and returns a Time. Sub takes two Time values and returns the Duration between them.

AddDate(years, months, days) is calendar arithmetic, not duration arithmetic, and it can overflow a month in a way that surprises AI-generated “add one month” code: time.Date(2026, 1, 31, ...).AddDate(0, 1, 0) does not clamp to the last day of February — it normalizes February 31st forward into March, producing March 3rd (2026 is not a leap year). Any code adding months to a date near month-end needs to account for this explicitly.

time.After, time.NewTimer, and time.NewTicker all deliver on a channel. Two semantics changed recently and are now the only behaviour on Go 1.27:

  • Timer channels are unbuffered. Since Go 1.23 (for go 1.23+ modules) a timer’s channel is synchronous: a value is only ever seen after the timer fires, and t.Stop() / t.Reset() guarantee no stale value is delivered afterwards — the old “drain the channel after Stop” ritual is gone. The asynctimerchan GODEBUG escape hatch was removed in Go 1.27, so every program gets this behaviour.
  • Unreferenced timers are garbage-collected. A time.After inside a select in a loop no longer leaks until it fires; once nothing references the timer it can be collected.
// given ctx context.Context
timer := time.NewTimer(50 * time.Millisecond)
select {
case <-timer.C:
fmt.Println("fired")
case <-ctx.Done():
timer.Stop() // no drain needed on Go 1.23+ semantics
}

Still prefer time.NewTicker over time.Tick in long-lived code so you can Stop() it, and context.WithTimeout over hand-rolled timers when the deadline belongs to a request.

Before Go 1.23, time.Tick was a guaranteed leak. It returns only a channel, with no handle to call Stop() on, so the ticker behind it could never be freed by the garbage collector — idiomatic pre-1.23 code always used time.NewTicker instead, specifically so it had something to Stop(). As of Go 1.23, the garbage collector can recover an unreferenced Ticker (or Timer) even without Stop(), and the standard library’s own documentation for time.Tick now says “there is no longer any reason to prefer NewTicker when Tick will do.” Stop() still matters when you need to actually halt delivery while other references to the ticker stay alive — it is no longer required just to avoid a leak. Code (or an AI assistant) that still insists “never use time.Tick, it always leaks” is repeating pre-1.23 folklore.

Code that calls time.Now() directly is hard to test deterministically — the “right” answer changes every time the test runs. The common fix is to accept a clock as a function value instead of calling time.Now() inline, so a test can substitute a fixed time:

func isExpired(now func() time.Time, deadline time.Time) bool {
return now().After(deadline)
}
// production: isExpired(time.Now, deadline)
// test: isExpired(func() time.Time { return fixedTime }, deadline)

This is the same dependency-injection idea used for a database connection or an HTTP client — pass the thing that varies as a parameter (or a small interface with a Now() time.Time method) instead of reaching for a global.

package main
import (
"fmt"
"time"
)
func main() {
// Fixed time — deterministic in any interpreter
t := time.Date(2024, 1, 2, 15, 4, 5, 0, time.UTC)
fmt.Println(t)
// Formatting with the reference layout
fmt.Println(t.Format("2006-01-02"))
fmt.Println(t.Format("15:04:05"))
fmt.Println(t.Format("Mon, 02 Jan 2006 15:04:05 MST"))
fmt.Println(t.Format(time.RFC3339))
// Parsing
parsed, err := time.Parse("2006-01-02", "2024-06-15")
if err == nil {
fmt.Println(parsed.Format("January 2, 2006"))
}
// Arithmetic
deadline := t.Add(72 * time.Hour)
fmt.Println(deadline.Format("2006-01-02 15:04:05"))
diff := deadline.Sub(t)
fmt.Println(diff)
// Duration construction
d := 2*time.Hour + 30*time.Minute
fmt.Println(d)
// Accessing date parts
fmt.Println(t.Year(), t.Month(), t.Day())
fmt.Println(t.Hour(), t.Minute(), t.Second())
}
package main
import (
"fmt"
"time"
)
func main() {
// 1. monotonic clock: Since/Sub use it even if the wall clock jumps;
// Equal compares the instant, == compares the whole struct (including monotonic reading + location)
a := time.Now()
b := a // identical copy
fmt.Println("a == b:", a == b, "a.Equal(b):", a.Equal(b))
c := a.Round(0) // strips the monotonic reading
fmt.Println("a == c (monotonic stripped):", a == c, "a.Equal(c):", a.Equal(c))
// 2. the n*time.Second unit bug: n is already a Duration once multiplied,
// but a bare integer like 5 passed to Sleep means 5 nanoseconds, not 5 seconds
var n time.Duration = 2
d := n * time.Second
fmt.Println("2 * time.Second:", d)
// time.Sleep(5) sleeps 5 nanoseconds — a classic unit bug, not "5 seconds"
start := time.Now()
time.Sleep(5)
fmt.Println("Sleep(5) slept for less than a millisecond:", time.Since(start) < time.Millisecond)
// 3. time.DateTime and friends (Go 1.20)
t := time.Date(2026, 9, 13, 14, 30, 0, 0, time.UTC)
fmt.Println("time.DateTime:", t.Format(time.DateTime))
fmt.Println("time.DateOnly:", t.Format(time.DateOnly))
fmt.Println("time.TimeOnly:", t.Format(time.TimeOnly))
// 4. Parse returns UTC; ParseInLocation uses the given location
p1, _ := time.Parse("2006-01-02 15:04:05", "2026-09-13 14:30:00")
fmt.Println("Parse location:", p1.Location())
loc, _ := time.LoadLocation("Asia/Bangkok")
p2, _ := time.ParseInLocation("2006-01-02 15:04:05", "2026-09-13 14:30:00", loc)
fmt.Println("ParseInLocation location:", p2.Location(), "same instant as UTC 07:30:", p2.UTC().Format(time.TimeOnly))
// 5. Ticker.Stop and AddDate month overflow
ticker := time.NewTicker(time.Hour)
ticker.Stop() // stops delivery now; not required for GC since Go 1.23, but still good practice
jan31 := time.Date(2026, 1, 31, 0, 0, 0, 0, time.UTC)
overflowed := jan31.AddDate(0, 1, 0) // "add one month" overflows into March
fmt.Println("Jan 31 + 1 month:", overflowed.Format(time.DateOnly))
// 6. injecting a clock for tests: accept a func() time.Time instead of calling time.Now directly
fixedClock := func() time.Time { return time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) }
isExpired := func(now func() time.Time, deadline time.Time) bool { return now().After(deadline) }
fmt.Println("isExpired with injected clock:", isExpired(fixedClock, time.Date(2025, 12, 31, 0, 0, 0, 0, time.UTC)))
}

Real output from this program on Go 1.26.5 (the boolean comparisons make this deterministic despite calling time.Now()):

a == b: true a.Equal(b): true
a == c (monotonic stripped): false a.Equal(c): true
2 * time.Second: 2s
Sleep(5) slept for less than a millisecond: true
time.DateTime: 2026-09-13 14:30:00
time.DateOnly: 2026-09-13
time.TimeOnly: 14:30:00
Parse location: UTC
ParseInLocation location: Asia/Bangkok same instant as UTC 07:30: 07:30:00
Jan 31 + 1 month: 2026-03-03
isExpired with injected clock: true

Note that every assertion above is a relationship (== vs Equal, “less than a millisecond”, a location name, a fixed calendar date) rather than a wall-clock value — the only way to keep a time.Now()-based example’s output the same on every run.

flowchart LR
  Now["time.Now()"] --> T["time.Time value"]
  T --> Wall["wall clock reading"]
  T --> Mono["monotonic reading"]
  Wall -- "affected by NTP / manual changes" --> Format["Format() / == comparison"]
  Mono -- "always moves forward" --> SinceSub["Since() / Sub()"]
  T -- "Round(0) strips monotonic" --> T2["Time value: wall clock only"]
Since/Sub read the monotonic clock; Format and == see the wall clock too
What you getBenefitCost
time.Time as a value typeImmutable, safe to copy and shareEvery Time carries a timezone you must account for
time.Duration (int64 nanoseconds)Unambiguous arithmetic — 5*time.SecondMust multiply by a named constant, not just write “5”
Reference layout (“2006-01-02”)Self-documenting once you know the exampleDifferent from other languages’ YYYY/MM/DD — you have to memorize the magic numbers
  • time.Now() always returns UTC — it depends on the machine’s local timezone; call time.Now().UTC() when you need UTC explicitly.
  • time.Sleep(1 * time.Second) is precise — the OS scheduler may sleep longer than requested, e.g. 1.05s — it is not hard real-time.
  • time.Equal is the same as == — time.Equal compares the instant in time (ignoring timezone); == compares the entire memory layout, timezone included.
  • time.Sleep(5) sleeps 5 seconds — it sleeps 5 nanoseconds; Sleep takes a time.Duration, and a bare integer literal is a nanosecond count, not a unit-agnostic “amount of time.”
  • time.Tick always leaks if you don’t call Stop() — true before Go 1.23, when an unreferenced Ticker could never be GC’d; since Go 1.23 the garbage collector recovers an unreferenced Ticker or Timer on its own, so this is now outdated advice (Stop() is still useful to halt ticking while a reference is kept alive, just not required to avoid a leak).
  • AddDate(0, 1, 0) always lands on the same day next month — near month-end it can roll over into the following month instead (Jan 31 + 1 month ≠ Feb 31, which does not exist).

💡 Real-world example

net/http.Server: http.Server{ReadHeaderTimeout: 5 * time.Second} sets a timeout as a time.Duration to stop a slow-header client from holding a connection open.

context.WithTimeout: creates a deadline that must propagate across function calls — for example, calling a downstream service and cancelling it automatically when time runs out.

net/http.TimeoutHandler: derives a context.WithTimeout from r.Context() and selects on ctx.Done() to cut off a request when the handler runs too long.

time code compiles regardless of whether the units or the timezone are right, so these are worth checking by hand every time:

  • Does time.Sleep, context.WithTimeout, or time.NewTimer take a bare integer literal instead of a value multiplied by a time.Xxx constant?
  • Does code compare two time.Time values with == instead of .Equal() — especially after one of them went through Parse, Round, or a database round-trip?
  • Does code call time.Parse on a timestamp that is known to represent local time in a specific timezone, when ParseInLocation is what is needed?
  • Does an AddDate(0, n, 0) (or AddDate(n, 0, 0)) run near month-end (or Feb 29) without accounting for calendar overflow?
  • Does a comment or explanation claim time.Tick/an unstopped Ticker “always leaks” — a true statement before Go 1.23, but no longer the whole story?
  • Does test code call time.Now() directly inside the function under test, instead of accepting an injected clock?

🐛 Spot the bug

func retryWithBackoff(attempt int, do func() error) error {
for i := 0; i < attempt; i++ {
if err := do(); err == nil {
return nil
}
time.Sleep(5) // wait 5 seconds before retrying
}
return fmt.Errorf("failed after %d attempts", attempt)
}
Show the answer

The comment says “5 seconds”, but time.Sleep takes a time.Duration — an int64 count of nanoseconds — and 5 with no unit attached means 5 nanoseconds, not 5 seconds. The retry loop effectively has no backoff delay at all; it hammers do() as fast as the CPU allows. Fix it by multiplying by the named constant:

time.Sleep(5 * time.Second)
What is Go's reference time used for formatting?
How long does this sleep for?
time.Sleep(5)
fmt.Println("done")
What does t.Sub(other) return?
What does this print?
a := time.Now()
b := a.Round(0)
fmt.Println(a == b, a.Equal(b))
time.Parse("2006-01-02 15:04:05", s) is used on a string known to be local time in Asia/Bangkok. What is wrong?
Why accept a now func() time.Time parameter instead of calling time.Now() inside a function you want to test?