Which time.Time operations strip its monotonic clock reading, and what changes once one has?
answer
- reinterpret, recompute, or serialise
- one arithmetic method keeps it
- Round with a zero argument is the idiom
- encoders have nowhere to put it
- afterwards comparisons quietly use the calendar
basics
~20 sOperations that reinterpret or recompute the wall clock strip it: UTC, Local, In, Round, Truncate, AddDate, and every encoding such as MarshalJSON or GobEncode. Add keeps it. Once stripped, Sub, Before, After and Equal fall back to the wall clock.
solid answer
~40 sThree groups of operations discard a `time.Time`'s monotonic reading. First, the ones that reinterpret the wall time for display — `t.UTC()`, `t.Local()`, `t.In(loc)` — because their whole point is the wall reading. Second, pure wall-clock computations: `t.AddDate(y, m, d)`, `t.Round(d)` and `t.Truncate(d)`; `t.Round(0)` is the documented idiom for stripping deliberately. Third, every serialisation — `MarshalJSON`, `MarshalText`, `MarshalBinary`, `GobEncode` — because the reading means nothing outside this process, and `Format` has no layout element for it. `t.Add(d)` is the notable exception: it advances both readings and keeps the monotonic one. Once either operand has been stripped, `Sub`, `Before`, `After`, `Equal` and `Compare` silently fall back to comparing wall clocks, so the measurement is only as trustworthy as the system clock.
code
go · 9 linest := time.Now() // has a monotonic reading
keep := t.Add(time.Second) // still monotonic: Add advances both readings
gone1 := t.UTC() // stripped: reinterprets the wall reading
gone2 := t.Round(time.Minute) // stripped: wall-clock computation
gone3 := t.AddDate(0, 0, 1) // stripped: calendar arithmetic
strip := t.Round(0) // the canonical deliberate strip
fmt.Println(keep, gone1, gone2, gone3, strip)go deeper
Know that converting, rounding or saving a time.Time can quietly change how it compares later, and that the safe move is to measure with the value time.Now gave you.
Be able to name the three groups that strip — reinterpretation, calendar computation, serialisation — call out Add as the exception, and state that comparisons then fall back to the wall clock.
Demonstrate that you audit where a stored instant crosses a boundary, and that you keep measurement values in-process while treating reloaded timestamps as reports rather than stopwatch readings.
Own the convention: decide which types in your codebase are allowed to carry a time.Time across a boundary at all, versus which should carry an explicit duration or an epoch field, so this behaviour is never load-bearing.
## Why anything strips it at all A `time.Time` from `time.Now()` holds a wall-clock reading and a monotonic reading. The monotonic reading is an offset from an arbitrary origin private to this process, so it is only meaningful when subtracted from another reading taken by the same process. Any operation whose result could escape that context, or whose result is defined purely in terms of the calendar, has to drop it — keeping it would attach a number that no longer corresponds to the value's wall time. ## The three groups **1. Reinterpreting the wall time.** `t.UTC()`, `t.Local()` and `t.In(loc)` change how the same instant is presented. They exist for their effect on the wall reading, so they return a value with the monotonic reading removed. This one surprises people, because `t.UTC()` feels like a no-op on an instant — it is, for the instant, but not for the value's comparison behaviour. **2. Wall-clock computations.** `t.AddDate(y, m, d)`, `t.Round(d)` and `t.Truncate(d)` are defined against the calendar, so they always strip. This gives Go its canonical strip idiom: `t = t.Round(0)` returns the same instant with no monotonic reading attached. Reach for it when you deliberately want a value that compares by wall clock — for example one you are about to use as a map key. **3. Serialisation.** `t.MarshalJSON`, `t.MarshalText`, `t.MarshalBinary` and `t.GobEncode` all omit the monotonic reading, and `Format` provides no way to print it. A decoded `time.Time` therefore never has one, even if you encoded and decoded inside a single process. This is the most common accidental strip, because it happens across a boundary you were not thinking about: a cache, a queue message, a database row, a config file, an HTTP response. ## The exception worth memorising `t.Add(d)` adds `d` to **both** readings and keeps the monotonic one. That is what makes deadline arithmetic work: `deadline := start.Add(5 * time.Second)` and later `time.Now().Before(deadline)` is a monotonic comparison, immune to the system clock being corrected in between. Contrast it with `t.AddDate(0, 0, 1)`, which is a calendar operation and strips. ## What changes after a strip The operations that prefer the monotonic clock — `Sub`, `Before`, `After`, `Equal`, `Compare`, and `time.Since`/`time.Until` built on them — need **both** operands to carry a reading. If either has been stripped, they fall back to the wall-clock readings. Nothing warns you. The code compiles, runs, and usually produces the right answer, because the wall clock is usually approximately right. It produces a wrong answer exactly when the wall clock moves during the interval you were measuring: a synchronisation daemon stepping the clock, a virtual machine resuming from suspend, a manual clock change. Then a measured duration can be too large, too small, or negative. A second consequence is on `==`, which compares the whole struct: a stripped value and an unstripped one are never `==` even though they denote the same instant. ## Checking in practice The `String` method prints the monotonic reading as an `m=+…` or `m=-…` suffix, so `fmt.Printf("%v", t)` is the whole diagnostic: suffix present means the value will still compare monotonically, suffix absent means it will not. When a duration looks impossible, print both endpoints and look at the suffixes before suspecting anything else. ## The rule of thumb Measure with values that never left the process and never went through a wall-clock operation. Store and transmit timestamps freely — just do not expect a timestamp that has been stored, transmitted, rounded, or converted to a location to still measure elapsed time reliably. If a value has to make that trip, keep the in-process `time.Time` for the measurement and treat the reloaded one as a report of when something happened, not as a stopwatch reading.
- Why does t.Add(d) keep the monotonic reading when t.AddDate does not?`Add` shifts by a fixed `time.Duration`, so it can advance both readings by the same amount and they stay consistent. `AddDate` is calendar arithmetic — months and days have variable length and depend on the location and on daylight-saving rules — so its result is defined only in wall-clock terms and there is no correct monotonic value to carry forward.
- You want a time.Time to behave purely as a wall-clock instant. How do you get one?Call `t = t.Round(0)`, the documented way to strip the monotonic reading while leaving the instant unchanged. Do that before using a `time.Time` as a map key or comparing it with `==`, and normalise the location too — for example `t.UTC().Round(0)` — since `==` also compares the location pointer.
- Does time.Since(start) still work if start was rounded to the second first?It compiles and returns something, but it is no longer a monotonic measurement: `Round` stripped the reading, so `Sub` falls back to the wall clocks. You also introduced up to a second of rounding error. Round for display or bucketing, and keep the original untouched value for measuring.
saying these in an interview costs you the question
- Believes t.UTC() only changes formatting and is otherwise a no-op
- Thinks the monotonic reading survives JSON or gob encoding
- Assumes Round and Truncate keep the monotonic reading
- Expects a compile error or warning when a comparison loses monotonic behaviour
- Claims t.Add strips the reading like t.AddDate does