Durations & Time
Assumes table-driven tests (05-practice/01-standard-library/01-testing) —
this lesson doesn't re-teach them. What's new here is time.Duration as a
value you can compute with, and how Go parses and formats calendar times.
Duration is just an int64
time.Duration is a named type over int64 counting nanoseconds. That
sounds like an implementation detail, but it's the reason duration
arithmetic reads so naturally:
d := 90 * time.Minute // 1h30m0s
d += 15 * time.Second // 1h30m15s
if d > time.Hour {
fmt.Println("running long")
}
time.Nanosecond, time.Second, time.Minute, time.Hour, and friends
are just typed constants, so 90 * time.Minute is ordinary constant
multiplication — no conversion, no helper function. Because it's a real
number under the hood, a Duration also has methods that pull values back
out: d.Hours(), d.Minutes(), d.Seconds() return float64, useful for
logging or metrics where you want "1.5" rather than "1h30m0s". Comparison
operators (<, >, ==) work directly, which is what makes durations so
natural in timeout logic.
Building times without time.Now()
You can construct a time.Time two ways. time.Now() reads the system
clock — convenient, but it makes anything downstream of it untestable: run
the same test twice and you get two different answers. time.Date(year, month, day, hour, min, sec, nsec, loc) builds an explicit, exact instant:
t := time.Date(2026, time.July, 12, 9, 30, 0, 0, time.UTC)
This is a theme the course keeps returning to: functions that take a time
in as a parameter are trivial to test, functions that reach out and call
time.Now() internally are not. Prefer the former whenever the caller can
supply it.
The reference-time layout
Most languages format dates with pattern codes like YYYY-MM-DD. Go does
something different: you write out one specific moment — Mon Jan 2 15:04:05 MST 2006 — arranged in the layout you want, and Go matches
positions against it. That moment is easy to remember because its
components count up: 01/02 03:04:05 PM '06 (month, day, hour, minute,
second, year). So "2006-01-02 15:04" means "four-digit year, dash,
two-digit month, dash, two-digit day, space, 24-hour hour, colon, minute" —
not a literal date, a shape. The standard library ships common shapes as
constants (time.RFC3339, time.DateOnly, time.DateTime), but you can
define your own the same way, which is what this exercise does.
Parse vs ParseInLocation, and why we pin UTC
time.Parse(layout, value) parses a string but defaults to UTC only when
the string carries no zone information — otherwise behavior depends on
what's embedded in value. time.ParseInLocation(layout, value, loc) is
explicit: whatever loc you pass is the zone unset offsets resolve to. For
this exercise, and generally when you want deterministic, sandbox-safe
code, pass time.UTC and mean it. For a fixed non-UTC offset — say, a
system that's always "UTC+2" — time.FixedZone("name", offsetSeconds)
builds a location without touching the filesystem. time.LoadLocation
exists for named zones like "Europe/Madrid" that observe daylight saving,
but it depends on a system timezone database that minimal containers often
don't ship with. Know it's there for real deployments; don't reach for it
in code you expect to run anywhere.
Add, Sub, Equal — and the == trap
t.Add(d) returns a new time shifted by a duration (negative durations
shift backward); t2.Sub(t1) returns the Duration between two times.
Both handle month/year/midnight rollover for you — that's the payoff of
representing time as a single count rather than separate y/m/d/h/m/s
fields you'd have to carry by hand. To compare two time.Time values, use
t1.Equal(t2), not t1 == t2. Times returned by time.Now() carry a
monotonic reading alongside the wall clock, and two values can also differ
by Location while naming the same instant — == compares the struct
byte-for-byte and trips on both. Equal compares the instants they
represent, which is almost always what you mean.
Practice
1. Shift the Schedule
IntermediateGo doesn't format times with
YYYY-MM-DDpatterns — you write the reference time (Mon Jan 2 15:04:05 MST 2006) in your desired layout, and Go pattern-matches against it.Layoutis already defined.ParseStamp(s string) (time.Time, error)— parsesinLayout, always in UTC (time.ParseInLocationwithtime.UTC).Shift(start string, d time.Duration) (string, error)— parsestart, move it byd(negative is allowed), format the result back inLayout. On a parse failure return""and the error.
time.Durationdoes the heavy lifting:t.Add(90 * time.Minute)handles hour, day, month, and year rollover for you — check the cross-midnight and New Year's cases in the tests.package main import ( "fmt" "time" ) // Layout is this exercise's timestamp format. Go layouts are written using // the reference time — Mon Jan 2 15:04:05 MST 2006 — so "2006" means "the // year goes here", not a literal 2006. const Layout = "2006-01-02 15:04" // ParseStamp parses s in Layout, always in UTC. func ParseStamp(s string) (time.Time, error) { // TODO: time.ParseInLocation with Layout and time.UTC — plain // time.Parse would give you UTC here too, but say what you mean return time.Time{}, nil } // Shift parses start, moves it by d (which may be negative), and formats // the result back in Layout. func Shift(start string, d time.Duration) (string, error) { // TODO: parse via ParseStamp — propagate its error — then // .Add(d).Format(Layout) return "", nil } func main() { out, err := Shift("2026-07-12 09:30", 90*time.Minute) if err != nil { fmt.Println("error:", err) return } fmt.Println("shifted:", out) }Sign in to write and run your solution in the browser.
Sign in for hints and the solution.