The Done channel

May 20, 2025

May I rant for a moment?

I don’t like the way GoDoc works for structs and interfaces. And below is a demonstration as to why. It’s not possible to link directly to an interface method or struct field, and the formatting is ugly. sigh

type Context

type Context interface {
…
	// Done returns a channel that's closed when work done on behalf of this
	// context should be canceled. Done may return nil if this context can
	// never be canceled. Successive calls to Done return the same value.
	// The close of the Done channel may happen asynchronously,
	// after the cancel function returns.
	//
	// WithCancel arranges for Done to be closed when cancel is called;
	// WithDeadline arranges for Done to be closed when the deadline
	// expires; WithTimeout arranges for Done to be closed when the timeout
	// elapses.
	//
	// Done is provided for use in select statements:
	//
	//  // Stream generates values with DoSomething and sends them to out
	//  // until DoSomething returns an error or ctx.Done is closed.
	//  func Stream(ctx context.Context, out chan<- Value) error {
	//  	for {
	//  		v, err := DoSomething(ctx)
	//  		if err != nil {
	//  			return err
	//  		}
	//  		select {
	//  		case <-ctx.Done():
	//  			return ctx.Err()
	//  		case out <- v:
	//  		}
	//  	}
	//  }
	//
	// See https://blog.golang.org/pipelines for more examples of how to use
	// a Done channel for cancellation.
	Done() <-chan struct{}

If you ever write code to honor context cancelation directly, you’ll be using the Done() method. And as the inline example demonstrates, it’s often used within a select statement, but that’s not the only way to use it. Before we look at usage examples, though, let’s dicuss some subtleties.

  1. Done returns a channel that’s closed when work done on behalf of this context should be canceled.

    Notice that no data is ever actually sent over the channel. This channel serves as a simple boolean flag: If the channel is open (or nil—see next point), keep working. If the channel has been closed, stop doing work.

  2. Done may return nil if this context can never be canceled.

    Contexts are not required to carry cancelation signals. They may carry only values, or maybe even nothing at all (as is the case with context.Background()). Such contexts may return a nil channel. Receiving from a nil channel blocks forever. So this is naturally the correct behavior—you likely never need to explicitly check for a nil channel returned from Done().

  3. Successive calls to Done return the same value.

    You never need to re-call Done() to see if something has changed.

  4. The close of the Done channel may happen asynchronously, after the cancel function returns.

    Note the phrasing: may. No guarantees here. When a context’s cancel() function is called, the context may cancel immediately, or it may cancel after a short delay, depending on the implementation of the context. So don’t use context cancelation for data synchronization. If you need data synchronization, use a mutex, atomic value, or some other mechanism.

Alright. With that out of the way, let’s look at the three common ways that the Done() method is used. First, and most simply, if you simply want to wait until a context is canceled, which is often done in a graceful shutdown type of situation:

func gracefulShutdown(ctx context.Context, s *http.Server) {
	<-ctx.Done() // Block until the Done channel is closed

	// Context is canceled, start shutting down the server.
	s.Shutdown()
}

Pretty straight forward, and quite powerful, for a particular type of use case.

But what if you want to do more than simply wait? What if you want to be doing actual work, but cancel the work-in-progress if the context is canceled? Let’s consider two examples of that.

First, similar to the example in the documentation above, let’s say the work to be done is waiting for a response over a channel:

// waitForResponse returns the first value sent on the in channel, or an error
// if the context is canceled first.
func waitForResponse(ctx context.Context, in <-chan int) (int, error)
for {
	select {
	case <-ctx.Done():
		return 0, ctx.Err():
	case val := <-in:
		return val, nil
	}
}

Simple enough—if your “work” to be done is passively listening for something to happen. But what if your work is something more invovled? Some sort of calculation? In this case, you can still check whether the context is canceled or not using a single-case select statement with an empty default:

select {
case <-ctx.Done():
	return ctx.Err():
default:
}

Without a default case, a select statement blocks until at least one of the case statements is unblocked. By adding a default, we tell the select statement to continue execution immediately, if one of (in this case the only) case statements is not currently ready to execute.

We can put such a statement at the beginning of a function, or at the top of a loop, for example, where we’re doing “real” work:

// fibonacci calculates the nth value in the fibonacci sequence, aborting when
// the context is canceled.
func fibonacci(ctx context.Context, n uint) (uint64, error) {
	if n == 0 {
		return 0, nil
	}
	if n <= 2 {
		return 1, nil
	}
	var a, b uint64 = 1, 1
	for n = n - 1; n > 0; n-- {
		select {
		case <-ctx.Done():
			return 0, ctx.Err()
		default:
		}
		a, b = b, a+b
	}
	return b, nil
}

Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .

Get daily content like this in your inbox!

Subscribe