AlgoMaster Logo

time Package

High Priority28 min readUpdated June 6, 2026

Every order has a timestamp. Every discount has an expiry. Every flash sale starts at one moment and ends at another. The time package is what Go gives you to work with all of that: the clock, the calendar, durations, time zones, and the timers that drive scheduled work. It looks tame on the surface and has more sharp edges underneath than almost any other standard library package, so this chapter walks through the parts you'll use and the surprises that surface in production.

time.Time: a Single Instant

A time.Time is a value that represents one specific instant. It carries a date (year, month, day), a clock reading (hour, minute, second, nanosecond), and a location (the time zone the clock reading is interpreted in). The zero value of time.Time is January 1, year 1, 00:00:00 UTC, which is also the date Go uses to mean "no time set".

The most common way to get one is time.Now(), which returns the current instant in your local time zone.

The output shows a date, a clock reading down to the microsecond, a time zone offset (+0530), a time zone name (IST), and a trailing m=+... value. That last part is the monotonic clock reading, which the package attaches to every time.Now() result. We'll come back to it later; for now, think of it as a separate stopwatch that lets two Time values be compared even if the wall clock got adjusted between them.

You don't always want "now". When you're building an order record from a database row, or constructing a discount expiry, you need to build a Time from explicit fields. time.Date is the function for that.

The argument order is year, month, day, hour, minute, second, nanosecond, location. The month is a time.Month constant (time.December is more readable than 12), and the location is a *time.Location.

Unix timestamps are another common source. Many databases and APIs store time as seconds (or milliseconds) since January 1, 1970 UTC. time.Unix(sec, nsec) builds a Time from those.

The reverse direction is t.Unix() (seconds), t.UnixMilli(), t.UnixMicro(), and t.UnixNano(). Each returns an int64. For most database columns, UnixMilli is the right choice because seconds-only resolution misses things like "two orders placed in the same second", which happens at any real traffic level.

Comparing Times

Two Time values can be compared with Before, After, and Equal. Don't use == for time comparison: it compares every internal field including the monotonic reading, so two times that represent the same instant but were built differently will compare unequal.

Equal checks that two times represent the same instant. It does the right thing even if one was built in UTC and the other in a local zone: as long as they refer to the same moment, Equal returns true. == would not, because the location field would differ.

time.Duration: a Length of Time

A Duration is the difference between two Time values, or simply a length of time. It's defined as int64 nanoseconds internally, so the maximum duration is about 290 years and the minimum resolution is one nanosecond.

The package exports unit constants you multiply with:

The constants are time.Nanosecond, time.Microsecond, time.Millisecond, time.Second, time.Minute, and time.Hour. There's no time.Day or time.Week, deliberately: a day isn't always 24 hours (daylight savings, leap seconds), so the package refuses to pretend otherwise. If you want "tomorrow at this time", use t.AddDate(0, 0, 1) instead of adding 24 hours.

Duration arithmetic is plain integer arithmetic: you can add, subtract, multiply by an integer, and divide.

The Duration type has methods to read it as a specific unit: d.Seconds(), d.Minutes(), d.Hours(), all returning float64. There's also d.Milliseconds(), d.Microseconds(), and d.Nanoseconds(), which return int64.

To get a duration between two times, use t2.Sub(t1). The result is a Duration. The other direction is t.Add(d) which returns a new Time shifted by d.

Sub and Add are the two methods you'll use most. There's also t.AddDate(years, months, days) for calendar-aware shifts, which handles month lengths and leap years correctly. Use it when you mean "one month from now", not "30 days from now".

January 31 plus one month is March 3, because February has 28 days and AddDate overflows to the next month (Jan 31 + 1 month = Feb 31, which normalizes to Mar 3). Thirty days lands on March 2. Neither is wrong, they're just different operations, and the one to use depends on what your product means by "expires in one month".

Parsing and Formatting: the Reference Layout

This is the part of the time package that catches most people the first time. Most languages format dates with placeholders like %Y-%m-%d or yyyy-MM-dd. Go uses a concrete reference date instead: you write what the output should look like for January 2, 2006, 3:04:05 PM Mountain Standard Time, and the package substitutes your real date into the same positions.

The reference time is:

Read it as: 1 2 3 4 5 6 7. January (1), 2nd (2), at 3:04:05 PM (15:04:05 in 24-hour), in time zone 7 (MST), in year 2006. The numbers count up so you can recall them in order. The mnemonics are awkward, but the reference date approach makes every layout self-documenting once you know it.

Here's the cheat sheet for the most common pieces:

ComponentReference valueMeaning
Year20064-digit year
Year062-digit year
Month01Zero-padded month number
Month1Month without padding
MonthJanShort month name
MonthJanuaryFull month name
Day02Zero-padded day
Day2Day without padding
WeekdayMonShort weekday name
WeekdayMondayFull weekday name
Hour1524-hour clock
Hour0312-hour clock, zero-padded
Hour312-hour clock
Minute04Zero-padded minute
Minute4Minute without padding
Second05Zero-padded second
AM/PMPMUppercase
AM/PMpmLowercase
Time zone offset-0700As +HHMM or -HHMM
Time zone offset-07:00As +HH:MM
Time zone nameMSTAbbreviation

Format a time.Time with t.Format(layout):

Parsing is the inverse, time.Parse(layout, value):

time.Parse assumes UTC when the layout doesn't include a time zone. If your input string is in a specific local zone, use time.ParseInLocation(layout, value, loc) instead, otherwise you'll get a Time that's wrong by the local UTC offset.

The package ships a handful of layout constants for the formats you see in network protocols and APIs.

ConstantValueUsed for
time.RFC33392006-01-02T15:04:05Z07:00The standard for JSON timestamps, log lines, most modern APIs
time.RFC3339Nano2006-01-02T15:04:05.999999999Z07:00RFC3339 with nanosecond precision
time.DateTime2006-01-02 15:04:05The most common human-readable format (Go 1.20+)
time.DateOnly2006-01-02Just the date (Go 1.20+)
time.TimeOnly15:04:05Just the time of day (Go 1.20+)
time.RFC1123Mon, 02 Jan 2006 15:04:05 MSTHTTP headers (older style)

RFC3339 is what encoding/json uses by default for time.Time fields, so it's the one you'll see most often.

A parsing error is the most common runtime issue with the time package. The error message is usually helpful: it tells you which character in the input didn't match the layout. The most common cause is mixing up 2 (day without padding) and 02 (zero-padded day), or using 15 when the input is actually a 12-hour clock.

Switching the layout to 2006-01-2 (no zero padding) accepts the input. Or you can pre-normalize the input string. Either way, the error tells you exactly which position is the problem.

Time Zones and Locations

A time.Location represents a time zone with its rules about UTC offset and daylight savings. The package provides three ways to get one.

time.UTC is the constant for Coordinated Universal Time. It's a *time.Location value you can pass anywhere a location is expected.

time.Local is the system's local time zone, as the OS reports it. time.Now() uses time.Local by default.

time.LoadLocation(name) loads a named zone from the IANA tz database, the same database that powers most operating systems' time zone handling. Names look like America/New_York, Asia/Kolkata, Europe/London.

t.In(loc) returns a new Time with the same instant but viewed in a different zone. The underlying moment doesn't change; only how it's interpreted when you call Hour(), Day(), or format it does.

The store opens at 9 AM local time and you have customers in multiple zones. The right way to think about scheduling is to store everything in UTC and convert for display. The wrong way is to store a Time with time.Local and forget that your server might run in one zone and your customer in another.

LoadLocation reads zone data from the OS's tz database, or from the bundled tzdata if the OS doesn't have one. On a typical Linux server this is /usr/share/zoneinfo. If you're shipping a container without zone data, import _ "time/tzdata" to bundle the database into the binary; it adds about 800 KB and ensures LoadLocation works anywhere.

For ad-hoc zones (a fixed offset you want without naming an IANA zone), time.FixedZone is the escape hatch.

FixedZone doesn't know about daylight savings; it's a flat offset. Use LoadLocation whenever you can, because real zones (like America/New_York) shift between EST and EDT during the year, and a FixedZone cannot.

The Monotonic Clock

Computers have two clocks. The wall clock is what time.Now() shows you and what gets adjusted when NTP corrects the system time, when daylight savings kicks in, or when the admin manually resets the date. The monotonic clock is a separate counter that only ever moves forward and never gets adjusted; it measures elapsed time without caring what the wall clock says.

time.Now() returns a Time that carries both readings. The wall part is what you see when you format the time. The monotonic part is invisible but gets used whenever you compute a duration between two Time values that both came from time.Now().

Why two clocks? Because the wall clock can move backwards. If your server's NTP daemon corrects a 200ms drift right after you took a start := time.Now() reading, then time.Now().Sub(start) using only the wall clock could give a negative duration. The monotonic clock prevents that: Sub between two monotonic-bearing times always returns elapsed real time, regardless of what NTP did.

time.Since(t) is shorthand for time.Now().Sub(t). It's the canonical way to measure how long an operation took. Because both start and the implicit time.Now() inside Since carry monotonic readings, the result is real elapsed time, immune to wall clock jumps.

The monotonic reading gets stripped in three situations: when you construct a Time with time.Date or time.Unix (no monotonic source), when you call t.Round(d) or t.Truncate(d) (rounding only makes sense on the wall clock), and when you explicitly call t.Round(0). The last form is the documented way to drop monotonic from a value while keeping the wall reading.

Why would you ever strip the monotonic reading? Two cases. First, when you serialize a Time and read it back, the deserialized version won't have a monotonic reading, and comparing the original to the round-tripped version with Sub could give a misleading number. Second, when you compare times across processes (one process's monotonic clock is meaningless to another), you want to compare wall readings only, so stripping monotonic first makes the comparison portable.

Use the result of time.Now() directly for durations measured in the same process. If you're storing a time, comparing across processes, or pulling a time out of a database, treat it as wall-clock only.

Timer, Ticker, After, Sleep

The time package gives you four ways to introduce a delay or fire on a schedule, each appropriate for a different shape of problem.

time.Sleep(d) blocks the calling goroutine for at least duration d. It's the simplest form, useful for retries with backoff, polling intervals, and rate limiting.

Sleep is fine when the goroutine has nothing else to do. The moment you want to wait OR react to a cancellation OR react to another event, you need a Timer or a Ticker so you can use select.

time.NewTimer(d) creates a single-shot timer that fires once after d. It exposes a channel t.C that receives the current time when the timer fires.

The single-shot pattern is what powers "this order auto-cancels in 30 minutes if unpaid" or "this discount expires in 24 hours". The timer fires once, you read from the channel, you act.

A timer is cancellable. timer.Stop() prevents it from firing if it hasn't already. The return value tells you whether the timer was active when you called Stop (true means you successfully stopped it before it fired; false means it had already fired or been stopped).

time.NewTicker(d) fires every d until you stop it. It's the right choice for periodic work: "every 30 seconds, check for new orders", "every 5 minutes, recompute the trending products list".

The defer ticker.Stop() is non-negotiable. A ticker holds a goroutine internally; if you stop reading from ticker.C without calling Stop, the goroutine keeps running forever and the ticker keeps trying to push values into a channel nobody reads. That's a leak. The standard pattern is defer ticker.Stop() right after you create one.

Second, ticker timing drifts. The ticker tries to fire every 300ms, but if your handler takes longer than that, ticks pile up and get dropped (the channel has a buffer of 1; extra ticks are silently dropped, not queued). For exact periodic timing, use a loop with time.NewTimer that you reset based on the expected next moment.

The Timer lifecycle has four states. You create one with NewTimer, it ticks down, and either fires (value appears on C) or you stop it before it fires. From either terminal state, Reset(d) puts it back to active. The order matters: in Go 1.23 and later, Reset after Stop is straightforward; in earlier versions, you had to drain the channel between Stop and Reset to avoid races, which was a notorious foot-gun. If you're targeting Go 1.23+ you can ignore that complication; otherwise check the package docs for the exact sequence.

time.After(d) is a convenience that combines NewTimer and reading from its channel into one expression. It returns a <-chan time.Time that fires once after d. The typical use is in select:

time.After looks clean and reads naturally, but every call allocates a fresh timer that runs to completion before being garbage collected, even when the surrounding select finished on a different case. Inside a hot loop, time.After leaks timers proportional to loop frequency.

Every loop iteration allocates a new 5-second timer. If 1,000 messages arrive per second, you have ~5,000 timers in flight at any moment, all pending. They aren't a correctness bug, but they show up as a memory hump in pprof, and on a small VM they can add real pressure. The fix is to create the timer once outside the loop and reset it:

Verbose, but correct, and one timer for the whole loop. Go 1.23 simplified the timer reset rules so the manual drain is no longer required in the common case, but the underlying lesson stands: in a hot loop, time.After is a leak you can measure.

Common Gotchas

The package has a few sharp corners that show up repeatedly.

`time.Sleep` is "at least", not "exactly". The OS scheduler picks when your goroutine resumes. On a busy machine, a 100ms sleep might be 110ms or 150ms. For most uses that's fine. For animation timing or low-latency work, it's a problem you solve at a different layer.

Ticker drift accumulates. If your handler takes longer than the tick interval, ticks get dropped, not queued. The ticker tries to fire on a fixed schedule (0, 1s, 2s, 3s...), and if you miss the 2s tick because you were busy, you don't get two ticks back-to-back; you get the 3s tick when 3s rolls around.

`time.After` in select loops leaks timers. Covered above. The fix is time.NewTimer outside the loop with Reset inside.

Timer/Ticker `Stop` doesn't drain the channel. If a timer has already fired by the time you call Stop, the value is sitting in the channel. Reading it again will block forever. The pre-Go-1.23 idiom was:

Go 1.23 made this unnecessary for new code; check the version you're targeting.

Don't use `==` to compare `Time` values. Use Equal. Two times for the same instant in different locations compare unequal with == but equal with Equal. This breaks tests where the expected and actual values came from different code paths.

`Format` and `Parse` panic-style mistakes. If your layout has 2006 and your input has 26, parsing fails. If your layout has 15:04 and your input is 2:00 PM, parsing fails. The error messages are precise: read them.

Don't store `time.Time` in a map key. A Time value with a monotonic reading hashes differently from the same wall instant without one, and the location field also factors in. If you need to key by an instant, key by t.UnixNano() or t.UTC().Round(0).

`Sub` on times from different processes can be wrong. Process A's monotonic clock means nothing to process B. If a Time round-trips through a database or RPC, the monotonic reading is gone, and Sub between such times gives only wall-clock difference, which is fine but not what the original Sub would have given.

Daylight savings is a calendar trap. "Add 24 hours" and "add 1 day" are different operations across a DST boundary. t.Add(24 * time.Hour) advances 24 real hours; t.AddDate(0, 0, 1) advances one calendar day, which might be 23 or 25 hours depending on the zone. Pick the right one for what your product means.

GotchaEffectFix
Ticker not stoppedGoroutine leakdefer ticker.Stop()
time.After in hot loopTimer leaktime.NewTimer + Reset outside loop
== on timesFalse negatives across zonesUse Equal
Drift on Sleep"Almost" the requested delayAccept it, or use a real scheduler
Monotonic stripped on serializeSub gives wall-only differenceDon't store and re-diff if you need monotonic
AddDate month overflowJan 31 + 1 month = Mar 3Be explicit about "last day of month" if needed
Missing tzdata in containerLoadLocation returns errorimport _ "time/tzdata"

Putting It Together

The following program ties most of the chapter together: a flash-sale checker that figures out whether a sale is live right now, how long until it ends, and prints those for two regional teams.

The sale is stored in UTC, the source of truth. Display conversions happen on the way out, never on the way in. The status method uses Before, After, and Sub rather than ad-hoc arithmetic, and rounds the duration to whole seconds for human-readable output. This is the shape of well-behaved time code: UTC everywhere internally, conversion at the edges, durations rounded for display.

Quiz

time Package Quiz

10 quizzes