How do you make a blocking call that doesn't accept a context (e.g. net.Conn.Read) cancelable?
Question 274HardGo 1.22 to 1.25
Turn the cancellation into something the blocking API does understand. For network connections that means setting a deadline in the past, which makes Read return at once with a timeout error. AfterFunc is the cleanest way to do it:
func readCtx(ctx context.Context, conn net.Conn, b []byte) (int, error) {
stopc := make(chan struct{})
stop := context.AfterFunc(ctx, func() {
conn.SetReadDeadline(time.Now()) // unblock the pending Read
close(stopc)
})
n, err := conn.Read(b)
if !stop() {
// AfterFunc already started: wait for it, then clear the deadline
// so the conn can be reused.
<-stopc
conn.SetReadDeadline(time.Time{})
return n, context.Cause(ctx)
}
return n, err
}
Subtle points:
- When
stop()returns false, the callback may still be running. Waiting onstopcensures it has finished setting the deadline before you reset it. Otherwise it could fire after your reset and break the nextRead. - Return the context error, not the resulting
os.ErrDeadlineExceeded, so callers see why the read stopped. - Other levers exist for other APIs: close the resource (
os.File, pipes),exec.CommandContextfor processes, orBroadcastforsync.Cond.
The older pattern starts a goroutine running select { case <-ctx.Done(): conn.Close(); case <-done: }. It works, but costs a goroutine per call and destroys the connection.
More on Context
- Q272context.Background() vs context.TODO(): when do you use each, and why not pass nil?
- Q273What is context.AfterFunc and what are the semantics of its stop function?
- Q275What does context.WithoutCancel do and when would you use it?
- Q276What is wrong with this handler? What error will the goroutine see?
- Q277When exactly is an http.Request's context canceled on the server side, and how do you use it correctly?
- Q278On the HTTP client side, how does context interact with http.Client.Timeout and response bodies?