Returning stop/cancel functions in #golang

Mat Ryer
2 min readApr 13, 2016

--

David Hernández and I added some metrics to our code because we were interested in how long key parts of our system took to execute. We set ourselves the challenge of coming up with the most elegant way to solve this, one that would make our other team members want to implement this new functionality (rather than having to.)

We started with this:

package metricfunc Time(name string, start, end time.Time) {
recordTimeMetric(name, start, end)
}

Which would be used like this:

func Something() {
start := time.Now()
// do work
metric.Time("something", start, time.Now())
}

Not bad.

Then we realised that we could simplify it for our users:

type Timer time.Timefunc Timer() *Timer {
return Timer(time.Now())
}
func (t Timer) Stop(name string) {
recordTime(name, time.Time(t), time.Now())
}

We took on a little pain so we could remove the thinking from the user, which let them do this:

func Something() {
timer := metric.Timer()
// do work
timer.Stop("something")
}

But we realised that passing the name into `Stop` feels wrong somehow. Although they could do this:

func Something() {
defer metric.Timer().Stop("something")
// do work
}

Now it’s a single line, where the Timer() call happens straight away, but the Stop call is deferred.

But that looks quite odd — and we didn’t think it was clear what was being called when.

We even considered something like this:

func Time(name string, fn func()) {
start := time.Now()
fn()
recordTime(name, start, time.Now())
}

But that would mean users would need to do wrap (and indent) their entire code body:

func Something() {
metric.Time("something", func(){
// do work
})
}

Returning a stop/cancel function

So after a few more iterations, we ended up with this:

func NewTimer(name string) func() {
start := time.Now()
return func() {
metric.recordTime(name, start, time.Now())
}
}

We have removed the need for the Timer type, and instead kept the start time and name state in a closure. The returned function completes the transaction.

Which allows users to do this:

func Something() {
stop := metric.NewTimer("something")
defer stop()
// do work
}

The NewTimer function returns a stop function that completes the operation. It’s obvious that we are deferring the stop, and the name is specified in a more sensible place — when the timer is created. The user doesn’t even need to import the time package. Users are also free to use timers in different ways:

func SomeBigFunction() {
// do some setup
stop := metric.NewTimer("first")
// do first thing
stop()
stop = metric.NewTimer("second")
// do second thing
stop()
// do some teardown
}

The net/context package makes use of this pattern for cancelling operations. Have you used a similar pattern anywhere? Share your experiences in the comments.

--

--

Mat Ryer

Founder at MachineBox.io — Gopher, developer, speaker, author — BitBar app https://getbitbar.com — Author of Go Programming Blueprints