Go: Goroutines and Apis
Goroutines are a unusual and powerful programming language feature, so they are a tempting toy to play with, and they get a bit overused.
There is some indication that the following Go principle holds true:
Strive to provide synchronous APIs,
let the caller start goroutines.
To put this advice into a more concrete code example:
Do this:
func (t *type) Run(ctx context.Context) {
// Implementation of background task
}
Instead of this:
func (t *type) Start() {
t.wg.Add(1)
go func() {
// Implementation of background task
t.wg.Done()
}
}
func (t *type) Cancel() {
// Somehow cancel the background task
t.wg.Wait()
}
Upsides for Run()
:
- The caller gets to decide how the “background” code gets run.
- Spawning Goroutines is easy for the caller too.
go
statements move towards the application’s top-level. Goroutine coordination becomes simpler to reason about when goroutines are started in fewer places.- Callers can also run it without goroutine and just block on the call, if there is no other work to be done.
- Background task cancellation is provided through the context.
- Waiting for the background task has a trivial API (when the function returns), and can be done with the mechanism the caller prefers (waitgroups, channels, …)
- It’s on the safe side API-wise: There is no
Close()
method which callers can forget to call (leaking resources).
Another great discussion of this was in the Go Time: On application design podcast (starting around minute 47, Mat Ryer’s explanation really resonated with me)
Addendum: In the comments, Jacob is pointing out the talk “Ways
To Do Things” by Peter
Bourgon
(slides),
who also appeared on the above “Go Time” episode. The talk describes
the Run()
style quite clearly and in more detail. Thanks for the
excellent pointer, Jacob!