24.2 time.Duration: Nanoseconds, Milliseconds, and Readable Literals
Let’s talk about time.Duration. It’s one of those things in Go that seems simple until you realize you’ve been doing it wrong for six months. At its heart, a Duration is just an int64 underneath, representing a span of time in nanoseconds. Yes, you read that right. Not milliseconds, not seconds, but nanoseconds. One billionth of a second. This choice is simultaneously brilliant and, for the uninitiated, a bit absurd. Why? Because it gives you integer precision over a massive range of time—from about 290 years down to a single nanosecond—without touching floating-point math and its attendant rounding errors. It’s the kind of brutally pragmatic design decision Go is famous for.
Why Nanoseconds?
The designers picked nanoseconds for a very Go-like reason: it’s the smallest common unit of time that can be represented as an integer without needing fractions. Milliseconds were too coarse for performance profiling and low-level system calls, and while picoseconds would have been even smaller, they’d be overkill for most practical purposes and would reduce the maximum representable duration. An int64 of nanoseconds gives you a range of approximately ±292 years. That covers everything from timing a function call to calculating the lease on a server rack. It’s a classic trade-off: enough precision for the finest measurements and enough range for the longest, all while keeping the type a simple, comparable, copyable integer.
The Readable Literals Are Your Best Friend
This is where Go’s syntax saves you from a lifetime of off-by-one-thousand errors. Instead of typing 1000000000 and hoping you got the right number of zeros, the time package defines constants for all the common units. This is not just sugar; it’s a fundamental best practice.
package main
import (
"fmt"
"time"
)
func main() {
// Don't do this. You *will* miscount the zeros.
awkwardDuration := 5000000000 // Is this 5 seconds? 50? Who knows!
// Do this instead. The compiler converts it to nanoseconds for you.
elegantDuration := 5 * time.Second
alsoElegant := 5000 * time.Millisecond // same thing
fmt.Println(awkwardDuration) // Prints: 5000000000 (useless)
fmt.Println(elegantDuration) // Prints: 5s (perfectly clear)
fmt.Println(alsoElegant) // Prints: 5s (also perfectly clear)
// You can combine them naturally.
totalTime := 2*time.Hour + 30*time.Minute + 15*time.Second + 100*time.Millisecond
fmt.Printf("The total time is %v.\n", totalTime) // The total time is 2h30m15.1s.
}
The ability to print a Duration in a human-readable format like 2h30m15.1s is a killer feature. It’s invaluable for debugging and logging. You’re not staring at a gigantic integer; you’re seeing exactly what you meant to define.
Converting to and from Other Units
This is the most common pitfall. Because a Duration is nanoseconds, you must convert to and from the units you actually care about. You can’t just cast an integer of milliseconds to a Duration. You must multiply by the correct unit constant.
func main() {
// How to convert from external integer milliseconds to Duration
externalMilliseconds := 1500
d := time.Duration(externalMilliseconds) * time.Millisecond
fmt.Println(d) // 1.5s
// How to convert a Duration to integer milliseconds
d = 2*time.Second + 500*time.Millisecond
milliseconds := d.Milliseconds() // Method returns an int64
fmt.Println(milliseconds) // 2500
// The same pattern applies for all units
seconds := d.Seconds() // returns a float64 because of sub-second precision
microseconds := d.Microseconds() // returns an int64
nanoseconds := d.Nanoseconds() // returns an int64 (the underlying value)
fmt.Printf("%vs, %vµs, %vns\n", seconds, microseconds, nanoseconds) // 2.5s, 2500000µs, 2500000000ns
}
The key here is to use the provided methods (Seconds(), Milliseconds(), etc.) to convert a Duration out to other units. To convert a raw integer in, you must multiply by the appropriate time constant. Forgetting that multiplication is a rite of passage for every Gopher. You’ll do it once, scratch your head why your timeout is a thousand times shorter than expected, and never make the mistake again.
The One Big Gotcha: API Compatibility
Here’s the questionable choice I warned you about. Many external libraries and even some parts of the standard library (looking at you, context) decided to represent durations in milliseconds as an integer. This creates a frustrating disconnect. You have this beautifully precise time.Duration type, and then you’re forced to decompose it back into an int64 of milliseconds to feed into these APIs.
func main() {
myTimeout := 5 * time.Second
// For a context, you have to convert to a Duration (which is fine)
ctx := context.WithTimeout(context.Background(), myTimeout) // Good.
// But for some library, you might have to convert to milliseconds
// Let's pretend SomeLibraryFunc expects an int64 of milliseconds
ms := myTimeout.Milliseconds()
SomeLibraryFunc(ms) // Annoying, but necessary.
// The real sin is when a function returns an int64 of milliseconds.
// You MUST remember to convert it back properly.
msFromSomeLibrary := int64(1500)
recoveredDuration := time.Duration(msFromSomeLibrary) * time.Millisecond // Remember this!
}
The best practice is to use time.Duration exclusively within your own code. Treat the boundaries where you receive or send raw integers as the exception, and wall them off with very clear conversions. Always ask yourself, “Is this value I’m about to use already a Duration?” If not, convert it at the point of entry. This discipline will save you from a world of pain.