“Do a thing, sleep a while, repeat” is an extremely common paradigm. Here is how that concept is typically expressed:

for {
  // this takes anywhere from a few milliseconds, to several seconds (or more)
  doOperation()
  time.Sleep(time.Second * 60) // do it every minute
}

The trouble is, it’s not really doing it every minute, it’s sleeping for a minute. Over time, depending on how long doOperation() takes, the consistency of your repetitions wanders, which might actually become problematic.

For example, say you have to record a data point every minute. If doOperation() runs at 04:59:58 and takes 3 seconds, the next call will happen at 5:51:01 - skipping the entire minute starting at 5:50.

For example:

package main

import (
	"log"
	"math/rand"
	"time"
)

func main() {
	for {
		log.Print("loop starting")

		doOperation()

		// wait and repeat
		time.Sleep(time.Second * 60)
	}
}

func doOperation() {
	// simulate indeterminate-length operation, 0-3 seconds ish
	time.Sleep(time.Millisecond * time.Duration(rand.Intn(3000)))
}

Might produce:

2022/04/03 10:59:57 loop starting
2022/04/03 11:00:59 loop starting
2022/04/03 11:02:01 loop starting
2022/04/03 11:03:03 loop starting

We missed a whole minute. Additionally (if you’re like me), it just looks offensive.

Of course, one could do some simple duration maths, calculating how long the operation took and sleeping for an appropriate time. But there is a much easier way:


func main() {
	for {
		timer := time.NewTimer(time.Second * 60)
		log.Print("loop starting")

		doOperation()

		// wait until timer expires
		<-timer.C
	}
}

Produces:

2022/04/03 11:06:51 loop starting
2022/04/03 11:07:51 loop starting
2022/04/03 11:08:51 loop starting
2022/04/03 11:09:51 loop starting

Beautiful.

This is the simplest use case of the time.Timer. Because it just exposes a channel which returns a time.Time value on timer expiry, you can do a lot more than this simple case.


Tags: loop  golang  time