How does Go's time.Date behave for a local time erased or repeated by a DST change?
answer
- one local hour vanishes, one happens twice
- the constructor reports nothing
- the docs guarantee only one of two zones
- round-trip the components you asked for
- compare offsets to see the repeat
basics
~10 stime.Date never reports the problem: for a local time erased by a spring-forward gap or repeated by a fall-back overlap, it returns a Time correct in one of the two offsets, without saying which.
solid answer
~50 sIn a zone with daylight saving, one local hour disappears each spring and one repeats each autumn. `time.Date(y, m, d, hh, mm, s, ns, loc)` accepts both cases without an error: the documentation says only that the result is correct in one of the two zones involved in the transition, and explicitly does not guarantee which. So a booking at 02:30 on a spring-forward day silently becomes some other instant, and 01:30 on a fall-back day silently picks one of two instants an hour apart. The detection is a round trip: build the `Time`, then compare its `Hour`, `Minute` and `Day` with what you asked for — a mismatch means the wall-clock time did not exist. For the repeat, compare `Zone()` offsets side by side, or use `ZoneBounds()` to see the transition. Then apply an explicit policy rather than letting the runtime choose.
code
go · 6 lines// In America/New_York the clocks jump 02:00 -> 03:00 on the spring transition,
// so 02:30 that morning is not a local time at all.
func existsLocally(y int, m time.Month, d, hh, mm int, loc *time.Location) bool {
t := time.Date(y, m, d, hh, mm, 0, 0, loc)
return t.Day() == d && t.Hour() == hh && t.Minute() == mm
}go deeper
Know that in a DST zone one local hour disappears each spring and another occurs twice each autumn, so a wall-clock time is not always a single instant.
Explain that time.Date silently resolves both cases, that the documentation only promises a result correct in one of the two zones, and how a component round trip reveals a gap.
Show how you would find this in production and prevent it: validate local times at input, compare Zone offsets or use ZoneBounds for ambiguity, recompute recurring instants per occurrence, and test the transition dates deliberately.
Own the resolution policy across services — what a gap and a repeat mean for the product, what is stored so the question stays answerable later, and who signs off when a region changes its DST rules.
## What a DST transition does to local time A zone that observes daylight saving shifts its UTC offset twice a year. In `America/New_York` the spring transition moves the clock from 02:00 straight to 03:00: **the local hour 02:00-02:59 does not exist that day**. The autumn transition moves 02:00 back to 01:00: **the local hour 01:00-01:59 happens twice**, once at the summer offset and once at the standard one. Every zone with DST has both, on dates set by local law, and the dates differ between regions. A local wall-clock time is therefore not a function of a date and a clock reading alone. In a gap it maps to *no* instant; in a repeat it maps to *two*. ## What Go does with that `func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time` normalises out-of-range components and returns a `Time`. It returns no error and never panics for a gap or a repeat. Its documentation states that in the ambiguous cases the choice of zone, and therefore the time, is not guaranteed: the result is correct in one of the two zones involved in the transition, but which one is unspecified. That is the crux for anything scheduling-shaped. Ask for 02:30 on the spring-forward morning and you get a real, valid `time.Time` — it simply is not the 02:30 you asked for, because there was none. Ask for 01:30 on the fall-back morning and you get one of two instants an hour apart, with no signal telling you the other existed. Both flow onward as ordinary values: they compare, format and store like everything else. The reason an on-call engineer hears about this the morning *after* a transition is that nothing failed; the data was merely built on a wall-clock time the runtime resolved for you. ## Detecting a gap Because `Date` normalises, a round trip is a reliable test: construct the value, then ask it what it thinks it is. ```go func existsLocally(y int, m time.Month, d, hh, mm int, loc *time.Location) bool { t := time.Date(y, m, d, hh, mm, 0, 0, loc) return t.Day() == d && t.Hour() == hh && t.Minute() == mm } ``` If the components come back different from what you supplied, the local time did not exist. This belongs in input validation, next to "is this date real", so a user booking an impossible local time is told at entry rather than at execution. ## Detecting a repeat A repeat cannot be spotted by round-tripping, because the value you get back *does* match what you asked for — twice over. Compare offsets instead. `Time.Zone()` returns the zone abbreviation and its offset in seconds east of UTC at that instant; the same wall-clock time an hour earlier and an hour later, examined through `Zone()`, reveals a transition sitting between them. `Time.ZoneBounds()` reports the start and end of the zone period in effect at a time, which locates the transition directly. If the two candidate instants have different offsets, the local time is ambiguous and something has to choose. ## Choosing, explicitly Once you can detect both cases, the remaining work is policy, and it is a product decision more than a technical one. Common resolutions: - **Gap:** reject at input, or push forward to the first instant that does exist (equivalent to "the appointment happens an hour later on the clock"). Whichever you pick, apply it in one place and tell the user what happened. - **Repeat:** pick the earlier of the two instants by convention, so a recurring job fires once rather than twice, and make idempotency the safety net rather than the offset choice. For recurring local-time events the underlying instant must be recomputed per occurrence with the zone in hand; adding a fixed duration to yesterday's instant walks straight through the transition and lands on the wrong wall-clock hour. ## What to carry through the system The values needed to resolve any of this are the local wall-clock time, the IANA zone name and the resolution rule. A computed instant alone cannot be re-resolved later, and an offset alone (`-05:00`) is not a zone: it says what the rule produced once, not what the rule is. Keep the zone name attached to the record that owns the local time, so the gap and repeat questions stay answerable.
- Why can't you detect a repeated local hour with the same round trip that finds a gap?Because the value round-trips cleanly. Both candidate instants report the wall-clock time you asked for, so the components match either way. Ambiguity shows up only in the UTC offset, which you reach through `Zone()` at neighbouring instants or through `ZoneBounds()`.
- A daily task must run at 09:00 local time. Why is adding 24 hours to yesterday's instant wrong?A local day is not always 24 hours long: across a transition it is 23 or 25. Adding a fixed duration lands on 08:00 or 10:00 local. The instant has to be recomputed from the local wall-clock time and the zone for each occurrence.
- Why is storing a UTC offset such as -05:00 not a substitute for the zone name?An offset records what a zone's rules produced at one moment; it cannot answer what they produce at another. Only the IANA name, such as `America/New_York`, lets you re-resolve a local time, detect a gap or a repeat, and stay correct when a region changes its rules.
- Does time.Date return an error for a local time inside a DST gap?No. `Date` has no error result and does not panic. It normalises and returns a `Time` that is correct in one of the two offsets around the transition, with the documentation explicitly declining to say which — so the caller must detect the case.
saying these in an interview costs you the question
- Expects time.Date to return an error for an impossible local time
- Assumes Go picks the earlier instant for a repeated hour
- Adds 24*time.Hour to keep a daily local-time schedule
- Stores a fixed UTC offset instead of the IANA zone name
- Believes UTC storage alone removes the gap and repeat problem
- Tests only dates far from any transition