Cancellation
Create a cancellable context with WithCancel, listen on Done(), and let one cancel stop everything derived below it.
ctx, cancel := context.WithCancel(parent); defer cancel()Cancellation is how one part of a program tells another to stop. Nothing is killed from outside. The work is told, and the work has to look.
Making a cancellable context
ctx, cancel := context.WithCancel(parent)
defer cancel()
context.WithCancel returns a child context and a function of type context.CancelFunc, which is just func(). Calling it cancels that child. It is safe to call from several goroutines at once, and safe to call more than once: every call after the first does nothing.
Listening for it
ctx.Done() returns a channel that is closed when the work should stop. A closed channel is always ready to receive, so every listener unblocks, however many there are. Put it in a select beside the real work:
select {
case job := <-jobs:
handle(job)
case <-ctx.Done():
return ctx.Err()
}
Once Done() is closed, ctx.Err() explains why. For a cancel it returns context.Canceled.
It only travels downwards
Cancelling a context cancels every context derived from it, and theirs in turn. One call at the top of a request stops the whole tree. Nothing travels up: cancelling a child leaves its parent running.
What trips people up
Call cancel even when the work finished normally. Until you do, the child stays registered on its parent and the parent holds onto it.
Go 1.20 added context.WithCancelCause. Its cancel function takes an error, and context.Cause(ctx) returns it. ctx.Err() still returns context.Canceled, so existing checks keep working.
Example
package main
import (
"context"
"fmt"
)
// count sends 1, 2, 3, ... until its context is cancelled.
// On cancellation it reports the reason on done and returns.
func count(ctx context.Context, out chan<- int, done chan<- error) {
for n := 1; ; n++ {
select {
case <-ctx.Done():
done <- ctx.Err()
return
case out <- n:
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
out := make(chan int)
done := make(chan error)
go count(ctx, out, done)
for i := 0; i < 3; i++ {
fmt.Println(<-out)
}
// Stop the goroutine, then wait for it to confirm that it stopped.
cancel()
fmt.Println(<-done)
}
// Output:
// 1
// 2
// 3
// context canceled
When to use it
- Shut down a pool of worker goroutines cleanly when the program receives a termination signal.
- Stop every replica request as soon as the first one answers.
- Abandon a long report build when the user navigates away.
- Tear down a background poller when the object that owns it is closed.
More examples
Cancelling the losers once one answer arrives
This is the shape of every hedged or fan-out request: start several attempts, take the first success, and stop paying for the rest. The buffered results channel matters. If it were unbuffered, the slower goroutines would block forever trying to send to a function that has already returned, and leak.
// first asks several replicas for the same thing and keeps the first reply.
func first(ctx context.Context, urls []string) (string, error) {
// A child context, so cancelling it cannot disturb the caller's.
ctx, cancel := context.WithCancel(ctx)
// Returning cancels every request still in flight.
defer cancel()
results := make(chan string, len(urls))
for _, u := range urls {
// Since Go 1.22 each iteration has its own u, so the goroutine
// sees the value from its own iteration.
go func() {
if body, err := get(ctx, u); err == nil {
results <- body
}
}()
}
select {
case body := <-results:
return body, nil
case <-ctx.Done():
return "", ctx.Err()
}
}Recording why, not just that (Go 1.20)
Plain cancellation loses the reason: everything downstream sees context.Canceled and cannot tell a shutdown from a quota breach from a client hanging up. WithCancelCause keeps the reason attached to the context so the goroutine that notices the cancellation can log something useful. Reach for it when more than one thing in your program can cancel the same work.
func serveTenant(parent context.Context, tenant int64) error {
ctx, cancel := context.WithCancelCause(parent)
defer cancel(nil)
if overQuota(tenant) {
cancel(fmt.Errorf("quota exhausted for tenant %d", tenant))
}
<-ctx.Done()
// ctx.Err() stays context.Canceled for every caller that checks it.
fmt.Println(ctx.Err())
// context.Cause reports the error the cancel function was given.
fmt.Println(context.Cause(ctx))
return context.Cause(ctx)
}Running cleanup when the context is cancelled (Go 1.21)
Some blocking calls do not take a context at all, and net.Conn.Read is the common one. AfterFunc bridges the gap: cancellation triggers a close, and the close is what unblocks the read. Use it whenever you need cancellation to reach an API that has no ctx parameter of its own.
func watch(ctx context.Context, conn net.Conn) error {
// AfterFunc calls f in its own goroutine once ctx is cancelled.
// Closing the connection makes a blocked Read return immediately.
stop := context.AfterFunc(ctx, func() {
conn.Close()
})
// stop unregisters f, and reports false if f has already started.
defer stop()
buf := make([]byte, 1024)
for {
n, err := conn.Read(buf)
if err != nil {
return err
}
handle(buf[:n])
}
}
Discussion