The surprising truth is that time.Now() in Go doesn’t always give you the wall-clock time.
Let’s see this in action. Imagine a Go program that needs to measure how long a specific operation takes. A naive approach might look like this:
package main
import (
"fmt"
"time"
)
func main() {
start := time.Now()
// Simulate some work
time.Sleep(100 * time.Millisecond)
end := time.Now()
duration := end.Sub(start)
fmt.Printf("Operation took: %v\n", duration)
}
If you run this, you’ll likely see output close to "Operation took: 100ms". This seems straightforward enough. time.Now() gives you a time.Time object, which has a UnixNano() method that returns the number of nanoseconds since the Unix epoch. Subtracting two UnixNano() values gives you the duration.
But what if the system clock changes while your program is running? This is where the magic (and the potential confusion) of Go’s time.Now() comes into play. Go’s time.Now() actually returns a time.Time value that is constructed using a monotonic clock in addition to the wall-clock.
The time.Time struct in Go has two underlying fields: sec (seconds since epoch) and nsec (nanoseconds within that second), which represent the wall-clock time. Crucially, it also has a hidden mono field (or rather, it derives monotonic time from the system). This mono field is based on a monotonic clock. A monotonic clock is one that always moves forward. It is not affected by system clock adjustments, NTP updates, or manual changes.
When you call time.Now(), Go internally captures both the current wall-clock time and the current value of the monotonic clock. When you then call end.Sub(start), Go doesn’t just subtract end.UnixNano() from start.UnixNano(). Instead, it calculates the difference based on the monotonic clock readings that were captured when each time.Now() call was made.
Why is this so important? Consider this scenario:
- Your program starts.
time.Now()records wall-clock time T1 and monotonic time M1. - The system clock is suddenly set backward by an hour (e.g., due to an NTP adjustment or a manual change).
- Your program continues to run and performs some operation.
- Later,
time.Now()is called again. It records wall-clock time T2 (which is now earlier than T1) and monotonic time M2 (which is guaranteed to be later than M1). - You calculate
end.Sub(start). If Go only used wall-clock time, subtracting T2 from T1 would yield a negative duration, which is nonsensical for measuring elapsed time. However, by using the monotonic clock, Go calculatesM2 - M1, which will correctly yield a positive duration representing the actual time that passed on the monotonic clock.
This behavior is fundamental to ensuring that time intervals measured by your Go program are reliable and not corrupted by external clock adjustments. It’s a robust design choice for measuring durations, even if it means time.Now() isn’t purely wall-clock time in its internal representation when used for duration calculations.
The time.Time struct internally stores a monotonic clock reading. This reading is used for duration calculations, ensuring that time differences are always positive and reflect the actual passage of time, irrespective of wall-clock adjustments.
The next thing you’ll likely encounter is how to deal with time zones and daylight saving time when performing time arithmetic, as this is another layer of complexity on top of basic duration measurement.