Go

The time package — instants carrying a Location, the reference layout, and no civil types.

Go’s standard library answers almost everything with a single type: time.Time, an absolute point in time that carries a time.Location along with it — the location doesn’t change which moment the value names, only how methods like Hour(), Day() and Format read it as civil time. time.Duration (an int64 of nanoseconds) covers exact spans. And that’s essentially the whole inventory: Go has no civil types at all — no date-only type, no zone-less wall-clock type. A “date” or a “time of day” exists only as a convention you impose on a time.Time (see What is a date?).

That design sidesteps the naive-datetime trap entirely — every time.Time is unambiguously a moment — but it moves the hazards elsewhere: into parsing (where a zone gets chosen for you) and into representing civil values that were never meant to be moments, like birthdays, which acquire an invented midnight and zone. See Points vs periods and Store vs display.

Formatting and parsing use the reference layout instead of pattern letters: you write what the reference moment Mon Jan 2 15:04:05 MST 2006 looks like in your format — "2006-01-02" is Go’s YYYY-MM-DD. Constants like time.RFC3339 and time.DateOnly cover the common cases; see RFC 3339.

The safe default for “now” is time.Now(); for storage convert to UTC explicitly (t.UTC()) and format with time.RFC3339.

Parsing chooses the zone, not you

time.Parse interprets a layout with no zone information as UTC — silently. If the string was a wall-clock reading from some zone, the resulting instant is off by that zone’s whole offset, and every later .In(loc) read compounds the confusion. time.ParseInLocation(layout, value, loc) is the version that lets you say which zone the civil reading belongs to.

Pitfall: time.Parse("2006-01-02 15:04", s) on a zone-less string returns the reading as UTC, whatever zone the string actually meant. Use time.ParseInLocation with an explicit *time.Location for civil input — and treat a layout without zone information in storage as a smell (see Your schema is a contract).

Pitfall: AddDate normalizes instead of clamping: January 31 + 1 month is March 3 (February 31 rolled forward), not February 28 — the same overflow behavior as legacy JavaScript Date. And d.Add(24 * time.Hour) advances exact time, not civil time: across a DST transition “24 hours later” is not “the same wall-clock time tomorrow” — that’s AddDate(0, 0, 1) on a time.Time whose Location is a real zone.

See also: Durations and Parsing user input.

Go deeper: the monotonic reading, layout gotchas, and living without civil types

The hidden monotonic clock. time.Now() embeds a monotonic clock reading alongside the wall reading. t2.Sub(t1) between two such values measures elapsed time immune to clock adjustments — great for what durations are for — but the extra reading makes == unreliable for time comparison. Compare with t1.Equal(t2), and strip the monotonic part with t.Round(0) when a value crosses a serialization boundary.

Reference-layout gotchas. The layout is a picture of one specific moment: 15 is 24-hour hour, 03 is 12-hour, Jan vs 01 picks name vs number, and a stray character that happens to appear in the reference (a 1, a 2…) is parsed as a field. Prefer the named constants:

t.Format(time.RFC3339)  // 2026-06-05T15:00:00-04:00
t.Format(time.DateOnly) // 2026-06-05

Living without civil types. For a date-only value (birthday, invoice date), the honest representations are a string ("2026-06-17"), a struct of ints, or — the common convention — midnight UTC via time.Date(y, m, d, 0, 0, 0, 0, time.UTC), provided every reader also reads it with .UTC(). Whichever you choose, document it in the schema. For “time zones vs offsets”, note time.FixedZone creates a fixed offset — only time.LoadLocation("America/New_York") gives you a real IANA zone with DST rules.

Zone data availability. LoadLocation reads the system tzdb, falling back to Go’s own copy; in scratch containers (or on Windows) add import _ "time/tzdata" to embed the database in the binary — the tzdb “bring your own data” tax made explicit.


← Back to all topics