Skip to content

golang context best practices

edeyoung edited this page Nov 27, 2019 · 5 revisions

The Basics

A go context is just a way to signal to an operation to stop, cleanup, and return as quickly as possible. See the documentation and blog post for a primer.

A context can be canceled or can time out (it can also hold values, but that's not recommended).

The base context is context.Background() context. This context never ends. If you need to create a cancellable (etc.) context, and don't have a context passed in (e.g., are in a main function), you should use context.Background() context as the parent. E.g.:

func main() {
  ctx, cancel := context.WithCancel(context.Background())
  ...
}

All context.With... commands create a new child context. Cancelling this context or having this context time out will not impact the parent context but will timeout or cancel any future child contexts of this child.

A context created with context.With... should always be canceled. To make sure this happens, write defer cancel after creation of the context. E.g.:

func main() {
  ctx, cancel := context.WithCancel(context.Background())
  defer cancel()
}

You should never provide a nil context to a function expecting a context. If you are drafting code and don't know what context should be sent in, you can use the context.TODO() context. If you don't care about cancelling the operation, send the context.Background() context.

Best Practices

  • A function should only take a context argument if there's a possibility that the function will be long running or spooling off long running processes.
  • Context handling should be done as close to the actual long running process as possible (example below).
  • If a function is calling something an operation takes in a context, let that operation do the context handling. There is no need to wrap the operation with additional context handling.
  • If a function is going to start a long running process, see how to manage the context for it in the example below.
// this function calls a long running function that handles a context.
// because the function handles the context, this function doesn't do any
// additional context handling (unless it has other long running processes
// it needs to handle).
func someFunc(ctx context.Context) error {
  // some code...

  err := longRunningProcessWithContext(ctx)
  if err != nil {
    return err // or wrap the error
  }

  // other code...
}

// this function calls a long running process that does not handle a context
// so this function needs to perform context handling around the long running
// process.
func longRunningProcessWithContext(ctx context.Context) error {
  // this checks if the context has completed before we start up the process
  select {
  case <-ctx.Done():
    return ctx.Err() // or perhaps you'll wrap the error
  default:
    // ok
  }

  // create a receiver channel for whatever the return type is of the process.
  // if the return type is compound, you may want to create a struct to create a
  // channel for. Channels do not support tuples.
  ch := make(chan error)
  // start the long running process in a go routine and send its result to the
  // created channel
  go func() { ch <- longRunningProcess() }()

  // handle whichever channel returns first - the result channel for longRunningProcess
  // or the context Done channel.
  select {
  case <-ctx.Done():
    // note: we are letting the process just finish in the routine because there is no 
    // way to cancel the process.
    // it is possible that we may want two wrappers around the process: one that returns
    // immediately once the context is complete so that the parent process can continue
    // and one that let's the process finish and handles any necessary cleanup.
    return ctx.Err() // or however you want to wrap the error
  case err <- ch:
    return err
  }
}

func longRunningProcess() error {
  // long running stuff
}

Some references that helped inform the best practices

https://golang.org/src/net/cgo_unix.go

	result := make(chan portLookupResult, 1)
	go cgoPortLookup(result, &hints, network, service)
	select {
	case r := <-result:
		return r.port, r.err, true
	case <-ctx.Done():
		// Since there isn't a portable way to cancel the lookup,
		// we just let it finish and write to the buffered channel.
		return 0, mapErr(ctx.Err()), false
	}
func cgoLookupCNAME(ctx context.Context, name string) (cname string, err error, completed bool) {
	if ctx.Done() == nil {
		_, cname, err = cgoLookupIPCNAME("ip", name)
		return cname, err, true
	}
	result := make(chan ipLookupResult, 1)
	go cgoIPLookup(result, "ip", name)
	select {
	case r := <-result:
		return r.cname, r.err, true
	case <-ctx.Done():
		return "", mapErr(ctx.Err()), false
	}
}

https://golang.org/src/net/dial.go

func (sd *sysDialer) dialSerial(ctx context.Context, ras addrList) (Conn, error) {
	var firstErr error // The error from the first address is most relevant.

	for i, ra := range ras {
		select {
		case <-ctx.Done():
			return nil, &OpError{Op: "dial", Net: sd.network, Source: sd.LocalAddr, Addr: ra, Err: mapErr(ctx.Err())}
		default:
		}

https://github.com/golang/go/issues/19856

@quentinmit, no they are not trying to wait, they are only doing a polling check. What they "should" be doing is:

var err error
select {
case <-ctx.Done():
    err = ctx.Err()
default:
   // ok
}
if err != nil

instead of if ctx.Err() != nil. But that's going to be a hard sell.

@alercah You're correct that Err can use exactly the same synchronization as Done under the hood, and in that respect it's not really any worse. The fact that they're so similar is the problem, though: I would bet that to the median programmer ctx.Err() looks like it should be a lot cheaper than select.

As for how to do that, I guess we could go to some trouble that finds places where code says:

err := ctx.Err()

and helpfully remind people that they should instead be writing:

var err error
select {
case <-ctx.Done():
    err = ctx.Err()
default:
   // ok
}

That doesn't seem like an improvement though. Better to redefine ctx.Err() to match existing usage, implementations, and expectations.