Go

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 on stopc ensures it has finished setting the deadline before you reset it. Otherwise it could fire after your reset and break the next Read.
  • 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.CommandContext for processes, or Broadcast for sync.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

All 35 Context questions