Go by Example

Timers

Schedule a one-shot event with time.NewTimer and cancel it early with Stop.

A time.Timer fires once after a specified duration. It exposes a channel C that receives a value when the timer expires. Call Stop to cancel before it fires.

Create a timer that fires after two seconds. Block on timer.C to wait for it. Use Stop in a second goroutine to cancel a timer before it fires - Stop returns false if the timer already fired.

package main
 
import (
    "fmt"
    "time"
)
 
func main() {
    t1 := time.NewTimer(2 * time.Second)
    <-t1.C
    fmt.Println("timer 1 fired")
 
    t2 := time.NewTimer(time.Second)
    go func() {
        <-t2.C
        fmt.Println("timer 2 fired")
    }()
    stopped := t2.Stop()
    if stopped {
        fmt.Println("timer 2 stopped before firing")
    }
}

time.AfterFunc runs a callback in a new goroutine when the timer expires - no channel receive required:

t := time.AfterFunc(500*time.Millisecond, func() {
    fmt.Println("callback fired")
})
// t.Stop() cancels the callback if called before the timer fires

When you need a one-shot delay inside a select and you may cancel early, prefer time.NewTimer over time.After:

timer := time.NewTimer(5 * time.Second)
defer timer.Stop() // prevent the timer goroutine from leaking
 
select {
case result := <-work:
    fmt.Println("got result:", result)
case <-timer.C:
    fmt.Println("timed out")
}

In production

Always call timer.Stop() and drain the channel if needed when cancelling early - the GC does not collect a timer that is still running. A common leak pattern: creating a timer in a loop without stopping the previous one before resetting it. If Stop returns false (the timer already fired), drain the channel to unblock future receives: select { case <-t.C: default: }.

Enjoyed this? Get more essays on software craft delivered to your inbox.

Subscribe free