Go’s built-in timer feature allows you to execute code at some point in the future. Timers are useful for scheduling one-time events, implementing timeouts, and creating cancellable delays.
Basic Timer Usage
package main
import (
"fmt"
"time"
)
func main() {
// Create a timer that fires after 2 seconds
timer1 := time.NewTimer(2 * time.Second)
// Block until the timer fires
<-timer1.C
fmt.Println("Timer 1 fired")
}
The timer’s channel C receives a value (the current time) when the timer expires.
Timer vs Sleep
Both wait for a duration, but timers can be cancelled:
// Simple delay - cannot be cancelled
time.Sleep(2 * time.Second)
// Timer - can be cancelled
timer := time.NewTimer(2 * time.Second)
<-timer.C
Use time.Sleep() for simple delays. Use time.NewTimer() when you need cancellation capability.
Cancelling a Timer
timer2 := time.NewTimer(time.Second)
go func() {
<-timer2.C
fmt.Println("Timer 2 fired")
}()
// Stop the timer before it fires
stopped := timer2.Stop()
if stopped {
fmt.Println("Timer 2 stopped")
}
// Give timer2 time to fire (to prove it was stopped)
time.Sleep(2 * time.Second)
Output:
The timer is cancelled, so “Timer 2 fired” is never printed.
Timer.Stop() Return Value
- true: Timer was stopped before it fired
- false: Timer already fired or was already stopped
Calling Stop() does not close the channel. If the timer has already fired, you may need to drain the channel to prevent blocking.
Draining a Timer Channel
if !timer.Stop() {
// Timer already fired, drain the channel
select {
case <-timer.C:
default:
}
}
Resetting a Timer
timer := time.NewTimer(1 * time.Second)
// Reset to fire after 2 seconds instead
timer.Reset(2 * time.Second)
<-timer.C
fmt.Println("Timer fired after reset")
Reset() should only be called on stopped or expired timers. For active timers, call Stop() first.
Safe Reset Pattern
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
timer.Reset(newDuration)
Practical Patterns
1. Timeout Implementation
func fetchWithTimeout(url string, timeout time.Duration) (string, error) {
result := make(chan string, 1)
errors := make(chan error, 1)
timer := time.NewTimer(timeout)
defer timer.Stop()
go func() {
data, err := fetch(url)
if err != nil {
errors <- err
return
}
result <- data
}()
select {
case data := <-result:
return data, nil
case err := <-errors:
return "", err
case <-timer.C:
return "", fmt.Errorf("timeout after %v", timeout)
}
}
func debounce(input <-chan string, delay time.Duration) <-chan string {
out := make(chan string)
go func() {
var timer *time.Timer
var lastValue string
for value := range input {
lastValue = value
if timer != nil {
timer.Stop()
}
timer = time.AfterFunc(delay, func() {
out <- lastValue
})
}
close(out)
}()
return out
}
3. Retry with Backoff
func retryWithBackoff(operation func() error, maxRetries int) error {
backoff := 1 * time.Second
for i := 0; i < maxRetries; i++ {
err := operation()
if err == nil {
return nil
}
if i < maxRetries-1 {
fmt.Printf("Attempt %d failed, retrying in %v\n", i+1, backoff)
timer := time.NewTimer(backoff)
<-timer.C
backoff *= 2 // Exponential backoff
}
}
return fmt.Errorf("max retries exceeded")
}
4. Rate Limiting
func rateLimiter(requests <-chan Request, rate time.Duration) {
for req := range requests {
processRequest(req)
// Wait before processing next request
timer := time.NewTimer(rate)
<-timer.C
}
}
time.After() Shorthand
For one-time use, time.After() is more convenient:
// time.After() creates and returns timer.C
select {
case result := <-ch:
process(result)
case <-time.After(5 * time.Second):
fmt.Println("timeout")
}
// Equivalent to:
timer := time.NewTimer(5 * time.Second)
select {
case result := <-ch:
process(result)
case <-timer.C:
fmt.Println("timeout")
}
time.After() cannot be cancelled and the timer won’t be garbage collected until it fires. Don’t use it in loops - use time.NewTimer() instead.
time.AfterFunc()
Execute a function after a duration:
timer := time.AfterFunc(2*time.Second, func() {
fmt.Println("Function executed after 2 seconds")
})
// Can still be cancelled
time.Sleep(1 * time.Second)
timer.Stop()
fmt.Println("Timer cancelled")
Comparison: Timer Functions
| Function | Returns | Can Cancel | Use Case |
|---|
time.NewTimer() | *Timer | Yes | Reusable, cancellable delays |
time.After() | <-chan Time | No | One-time timeout in select |
time.AfterFunc() | *Timer | Yes | Execute function after delay |
time.Sleep() | Nothing | No | Simple blocking delay |
Reusable Timer Pool
type TimerPool struct {
pool sync.Pool
}
func NewTimerPool() *TimerPool {
return &TimerPool{
pool: sync.Pool{
New: func() interface{} {
return time.NewTimer(0)
},
},
}
}
func (tp *TimerPool) Get(d time.Duration) *time.Timer {
timer := tp.pool.Get().(*time.Timer)
timer.Reset(d)
return timer
}
func (tp *TimerPool) Put(timer *time.Timer) {
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
tp.pool.Put(timer)
}
Heartbeat Pattern
func heartbeat(interval time.Duration, quit <-chan struct{}) <-chan time.Time {
heartbeats := make(chan time.Time)
go func() {
defer close(heartbeats)
timer := time.NewTimer(interval)
defer timer.Stop()
for {
select {
case <-quit:
return
case t := <-timer.C:
heartbeats <- t
timer.Reset(interval)
}
}
}()
return heartbeats
}
Deadline Pattern
func processWithDeadline(deadline time.Time) error {
timeout := time.Until(deadline)
if timeout <= 0 {
return errors.New("deadline already passed")
}
timer := time.NewTimer(timeout)
defer timer.Stop()
result := make(chan error, 1)
go func() {
result <- doWork()
}()
select {
case err := <-result:
return err
case <-timer.C:
return errors.New("deadline exceeded")
}
}
Common Mistakes
Memory Leak with time.After in Loop
// BAD: Creates many timers that aren't GC'd
for {
select {
case msg := <-messages:
process(msg)
case <-time.After(1 * time.Second): // LEAK!
timeout()
}
}
// GOOD: Reuse timer
timer := time.NewTimer(1 * time.Second)
defer timer.Stop()
for {
timer.Reset(1 * time.Second)
select {
case msg := <-messages:
process(msg)
case <-timer.C:
timeout()
}
}
Not Draining After Stop
// BAD: May block on receive
timer := time.NewTimer(1 * time.Second)
go func() {
<-timer.C // May block forever if stopped
fmt.Println("Fired")
}()
timer.Stop()
// GOOD: Check stop result and drain
timer := time.NewTimer(1 * time.Second)
if !timer.Stop() {
<-timer.C // Drain the channel
}
Resetting Active Timer
// BAD: Resetting active timer
timer := time.NewTimer(1 * time.Second)
timer.Reset(2 * time.Second) // Unsafe!
// GOOD: Stop before reset
timer := time.NewTimer(1 * time.Second)
if !timer.Stop() {
<-timer.C
}
timer.Reset(2 * time.Second)
Timers are lightweight. Creating a timer costs ~100-200 ns. However, in tight loops with millions of iterations, consider timer pooling.
Benchmarks (approximate)
- Creating timer: ~100-200 ns
- Stopping timer: ~20-30 ns
- Resetting timer: ~30-50 ns
- time.Sleep(): ~1000 ns overhead
Best Practices
- Use time.Sleep for simple delays - No need for timer if you can’t cancel
- Defer timer.Stop() - Always clean up timers
- Avoid time.After in loops - Creates memory leaks
- Drain channel after Stop - Prevent blocking on already-fired timers
- Stop before Reset - Safer to stop and drain before resetting
- Use buffered channels with timers - Prevent goroutine leaks
- Consider context.WithTimeout - More composable for cancellation
Timer vs Ticker
| Feature | Timer | Ticker |
|---|
| Fires | Once | Repeatedly |
| Use case | One-time event | Periodic events |
| Reset | Changes next fire time | Changes interval |
| Example | Timeout, delay | Heartbeat, polling |
- Tickers - Repeating timers for periodic events
- Timeouts - Implementing timeouts with timers
- Select - Using timers in select statements
- Context - Cancellation and deadlines