← O-Reilly Learning Go

Context

GolangContext

Overview

Context is used to pass values and control routines.
If a program is passing context to many levels, that means the program need to be refactored.

Value

The context.WithValue function returns a context, but it is not the same context that was passed into the function. Instead, it is a child context that contains the key-value pair and wraps the passed-in parent context.Context.

A context is treated as an immutable instance. Whenever you add information to a context, you do so by wrapping an existing parent context with a child context. This allows you to use contexts to pass information into deeper layers of the code. The context is never used to pass information out of deeper layers to higher layers.

If you are familiar with data structures, you might recognize that searching for values stored in the context chain is a linear search. This has no serious performance implications when there are only a few values, but it would perform poorly if you stored dozens of values in the context during a request. That said, if your program is creating a context chain with dozens of values, your program probably needs some refactoring.

Choosing context key wisely

Like Maps context keys can be collide.
So, we have to choose a certain method to prevent that.
There are two ways to solve that.

1. Use Int and iota

type userkey int
const (
	_ userkey iota
	key
)

func ContextWithUser(ctx context.Context, user string) context.Context {
	return context.WithValue(ctx, key, user)
}

Use empty struct as key

type userKey struct{}
func ContextWithUser(ctx context.Context, user string) context.Context {
	return context.WithValue(ctx, userKey{}, user)
}

How do you know which key style to use? If you have a set of related keys for storing different values in the context, use the int and iota technique. If you have only a single key, either is fine. The important thing is that you want to make it impossible for context keys to collide.

Cancellation

If you call Done on a context that isn’t cancellable, it returns nil. A read from a nil channel never returns. If this is not done inside a case in a select statement, your program will hang.

WithCancelCause

This is used and has ability to output an error when the context is cancelled.

resp, err := makeRequest(ctx, "http://httpbin.org/delay/1") 
if err != nil { fmt.Println("in delay goroutine:", err)
	cancelFunc(fmt.Errorf("in delay goroutine: %w", err)) 
	return 
} 
ch <- "success from delay: " + resp.Header.Get("date")

loop: for { 
	select { 
		case s := <-ch: fmt.Println("in main:", s) 
		case <-ctx.Done(): 
				fmt.Println("in main: cancelled with error", context.Cause(ctx))
				break loop 
		} 
	} 
	wg.Wait() 
	fmt.Println("context cause:", context.Cause(ctx))

#output
in main: success from status 
in main: success from delay: Thu, 16 Feb 2023 04:11:49 GMT 
in main: cancelled with error bad status 
in delay goroutine: Get "http://httpbin.org/delay/1": context canceled 
context cause: bad status

You see that the error from the status goroutine is printed out both when cancellation is initially detected in the switch statement and after you finish waiting for the delay goroutine to complete. Notice that the delay goroutine called cancelFunc with an error, but that error doesn’t overwrite the initial cancellation error.

GOMEMLIMIT provides a soft way to limit the amount of memory a Go program uses, if you want to enforce constraints on memory or disk space that a single request uses, you’ll have to write the code to manage that yourself. Discussion of this topic is beyond the scope of this book.

ContextWithDeadlines

Any timeout that you set on the child context is bounded by the timeout set on the parent context; if a parent context times out in 2 seconds, you can declare that a child context times out in 3 seconds, but when the parent context times out after 2 seconds, so will the child.