54. `time`
Go's time package has one famously strange design decision and a lot of
very good ones. This lesson covers both, plus the parts you'll use daily:
durations, formatting, arithmetic and measuring how long things took.
Two types: Time and Duration
package main
import (
"fmt"
"time"
)
func main() {
t := time.Date(2024, time.March, 15, 10, 30, 0, 0, time.UTC)
fmt.Println(t)
fmt.Println(t.Year(), t.Month(), t.Day())
fmt.Println(t.Hour(), t.Minute(), t.Weekday())
fmt.Println(t.YearDay(), t.Unix())
d := 90 * time.Minute
fmt.Println(d, d.Hours(), d.Minutes(), d.Seconds())
}
time.Timeis an instant. It's a value type, safe to copy, and immutable — every method returns a newTime.time.Durationis a span of time, stored as anint64count of nanoseconds.
We use a fixed date here so the output is stable; in real code you'd start
with time.Now().
Durations are numbers with units
This is the design that makes time pleasant:
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println(time.Second, time.Millisecond, time.Hour)
timeout := 30 * time.Second
retry := 500 * time.Millisecond
long := 2*time.Hour + 45*time.Minute
fmt.Println(timeout, retry, long)
fmt.Println("retries that fit in the timeout:", int64(timeout/retry))
fmt.Println(long.Round(time.Hour))
parsed, err := time.ParseDuration("1h30m")
fmt.Println(parsed, parsed.Minutes(), err)
}
Because Duration is an integer type with constants attached, 30 *
time.Second is ordinary multiplication, and it prints as 30s thanks to a
String() method. No "is this milliseconds or seconds?" — the units are in
the type.
One gotcha: multiplying by a variable needs a conversion, because Go
won't mix int and Duration:
package main
import (
"fmt"
"time"
)
func main() {
n := 5
// d := n * time.Second // won't compile
d := time.Duration(n) * time.Second
fmt.Println(d)
ms := 250
fmt.Println(time.Duration(ms) * time.Millisecond)
}
The reference-time layout
Here's the famous part. Go doesn't use YYYY-MM-DD. It formats by example,
using one specific reference time:
Mon Jan 2 15:04:05 MST 2006
| | | | | | | |
| | | | | | | +-- 2006 = year
| | | | | | +------ MST = timezone
| | | | | +---------- 05 = second
| | | | +------------- 04 = minute
| | | +---------------- 15 = hour (24h)
| | +------------------- 2 = day
| +---------------------- Jan = month
+-------------------------- Mon = weekday
The numbers are a mnemonic: 1 2 3 4 5 6 7 — month, day, hour, minute, second, year, timezone offset.
package main
import (
"fmt"
"time"
)
func main() {
t := time.Date(2024, time.March, 15, 14, 30, 45, 0, time.UTC)
fmt.Println(t.Format("2006-01-02"))
fmt.Println(t.Format("02/01/2006 15:04"))
fmt.Println(t.Format("Jan 2, 2006 at 3:04 PM"))
fmt.Println(t.Format("Monday, January 2"))
fmt.Println(t.Format(time.RFC3339))
fmt.Println(t.Format(time.RFC1123))
fmt.Println(t.Format("15:04:05.000"))
}
You write the layout as how the reference time should look. It's odd for
five minutes and then genuinely easier than remembering whether MM is
months or minutes.
The built-in layouts cover most cases — time.RFC3339 is what you want for
APIs, logs and anything machine-readable.
Parsing
Same layout string, in reverse:
package main
import (
"fmt"
"time"
)
func main() {
t, err := time.Parse("2006-01-02", "2024-03-15")
fmt.Println(t.Format(time.RFC1123), err)
_, err = time.Parse("2006-01-02", "15/03/2024")
fmt.Println("mismatch ->", err != nil)
ts, _ := time.Parse(time.RFC3339, "2024-03-15T14:30:45Z")
fmt.Println(ts.Hour(), ts.Minute())
}
time.Parse returns an error when the input doesn't match the layout — one
more (value, error) pair to check.
Arithmetic
package main
import (
"fmt"
"time"
)
func main() {
t := time.Date(2024, time.March, 15, 10, 0, 0, 0, time.UTC)
fmt.Println(t.Add(90 * time.Minute).Format("15:04"))
fmt.Println(t.Add(-2 * time.Hour).Format("15:04"))
fmt.Println(t.AddDate(0, 1, 0).Format("2006-01-02"))
fmt.Println(t.AddDate(1, 0, -15).Format("2006-01-02"))
later := t.Add(36 * time.Hour)
diff := later.Sub(t)
fmt.Println(diff, diff.Hours())
fmt.Println(later.After(t), later.Before(t), later.Equal(t))
}
Addtakes aDuration— good for hours and minutes.AddDate(years, months, days)handles calendar arithmetic, including month lengths and leap years.Subgives you aDurationbetween two times.- Compare with
After,Before,Equal— not==, which also compares the monotonic clock reading and the location pointer.
Measuring elapsed time
package main
import (
"fmt"
"time"
)
func main() {
start := time.Now()
total := 0
for i := 0; i < 200000; i++ {
total += i
}
elapsed := time.Since(start)
fmt.Println("sum:", total)
fmt.Println("took a measurable amount of time:", elapsed > 0)
fmt.Println("under a second:", elapsed < time.Second)
}
time.Since(start) is shorthand for time.Now().Sub(start), and it's the
standard way to time an operation. A time.Time from time.Now() carries a
monotonic clock reading, so Since stays correct even if the system
clock is adjusted mid-measurement.
The deferred-timer idiom, using closures from module 3:
package main
import (
"fmt"
"time"
)
func timed(name string) func() {
start := time.Now()
return func() {
fmt.Printf("%s finished in under a second: %t\n", name, time.Since(start) < time.Second)
}
}
func work() {
defer timed("work")()
sum := 0
for i := 0; i < 100000; i++ {
sum += i
}
}
func main() {
work()
}
Note the double parentheses in defer timed("work")(): timed("work") runs
now (recording the start), and the function it returns is what gets
deferred.
Sleeping, timers and tickers
package main
import (
"fmt"
"time"
)
func main() {
start := time.Now()
time.Sleep(20 * time.Millisecond)
fmt.Println("slept at least 20ms:", time.Since(start) >= 20*time.Millisecond)
select {
case <-time.After(10 * time.Millisecond):
fmt.Println("time.After fired")
}
ticker := time.NewTicker(5 * time.Millisecond)
defer ticker.Stop()
ticks := 0
for range ticker.C {
ticks++
if ticks == 3 {
break
}
}
fmt.Println("ticks received:", ticks)
}
time.After returns a channel — that's what made the timeout select in the
concurrency module work. time.NewTicker fires repeatedly on its C
channel; always defer ticker.Stop(), or its goroutine and timer leak.
Time zones
package main
import (
"fmt"
"time"
)
func main() {
t := time.Date(2024, time.March, 15, 14, 0, 0, 0, time.UTC)
fmt.Println(t.Format("15:04 MST"))
fmt.Println(t.UTC().Format(time.RFC3339))
fmt.Println(t.Unix(), "seconds since the epoch")
local := t.In(time.Local)
fmt.Println("same instant, local wall clock:", local.Unix() == t.Unix())
}
A time.Time always carries a location. In(loc) changes how it displays
without changing the instant — which is why the Unix timestamps match.
The professional habit: store and transmit UTC, convert to local only for
display. time.Now() gives you local time, so servers usually call
time.Now().UTC().
Your turn
Given a start date, print the date 45 days later and the number of hours between them:
2024-04-29
1080
package main
import (
"fmt"
"time"
)
func main() {
start := time.Date(2024, time.March, 15, 0, 0, 0, 0, time.UTC)
// print start+45 days as 2006-01-02, then the hours between them
}
package main
import (
"fmt"
"time"
)
func main() {
start := time.Date(2024, time.March, 15, 0, 0, 0, 0, time.UTC)
end := start.AddDate(0, 0, 45)
fmt.Println(end.Format("2006-01-02"))
fmt.Println(int(end.Sub(start).Hours()))
}
Next: talking to the outside world in JSON.