TL;TR
- How to use Context:
- Avoid using empty
Context
, i.e.Background
andTODO
, directly - Treat
Context
in params, and never put it in truct. - As a param of the function, Context should always be the first param.
- The
Value
related function of Context should be used for passing only-needed data. - Context is thread-safe and can be passed around in multiple goroutines.
- Avoid using empty
0. Intro
0.1 Why context
Context is an important standard package in Golang, as it provides clear and effective management to concurrent operations. Context provides a way to control the lifecycle, cancellation, and propagation of requests across multiple goroutines.
We all know when we start a gorountine, we cannot control but wait for it to terminate itself. Using the method of chan
+select
is an elegant way, but with limitations. In a more complex case where multiple goroutines need the termination control or each of these goroutines spawn additional goroutines, managing multiple channels or channels with a complex relationship tree is impossible to to be done in an effective and clean way.
0.2 What is context
Context provides us a clean solution to control goroutines by tracing them. It enables the propagation of cancellation signals, deadlines, and values across goroutines. Thus, we can create a hierarchy of goroutines and pass important info down the chain.
main functions in context
AfterFunc
WithCancel
WithDeadline
WithTimeout
WithValue
Don’t worry if you don’t understand it right now. We would dive into them later.
1. Let’s code
1.0 example of context’s controlling multiple goroutines
In this example, we start 3 monitor goroutine for periodic monitoring, and each is tracked by the same Context
. When we use cancel function to notify the canceling, these three goroutines would be terminated. The spawned sub-Context
s of the three would also be notified, and thus be cleaned up and be released, which fixes the control issue of goroutine gracefully.
1func MultipleWatch() {2 ctx, cancel := context.WithCancel(context.Background())3 go watch(ctx, "[Monitor1]")4 go watch(ctx, "[Monitor2]")5 go watch(ctx, "[Monitor3]")6
7 time.Sleep(3 * time.Second)8 fmt.Println("It is time to terminate all the monitors")9 cancel()10 time.Sleep(2 * time.Second)11}12
13func watch(ctx context.Context, name string) {14 for {15 select {9 collapsed lines
16 case <-ctx.Done():17 fmt.Println(name, "stopped monitoring at", time.Now().Format("15:04:05"))18 return19 default:20 fmt.Println(name, "monitoring at", time.Now().Format("15:04:05"))21 time.Sleep(1 * time.Second)22 }23 }24}
1.1 Context struct
1type Context interface {2 Deadline() (deadline time.Time, ok bool)3 Done() <-chan struct{}4 Err() error5 Value(key interface{}) interface{}6}
Deadline
: returns the time when work done on behalf of this context should be canceled.ok
is false when no deadline is set.Done
: returns a read-only chan, serving as a signal mechanism for cancellation. OnceDone
is readable, it means we recieves cancel signal.Err
:- If
Done
is not yet closed: returnsnil
. - If
Done
is closed: returns a non-nil error.
- If
Value
: returns the value associated with this context for key, ornil
.
1.2 Create a root context
1var (2 background = new(emptyCtx)3 todo = new(emptyCtx)4)5
6func Background() Context {7 return background8}9
10func TODO() Context {11 return todo12}
Both context.Background()
or context.TODO()
to return a non-nil, empty Context:
Background
is more frequently used as the top-level Context in main function, initialization and tests.TODO
is used when it’s unclear which Context to use or it is not yet available.
Since the two are emptyCtx
:
- they cannot be cancelled
- does not have a deadline set
- does not carry any values
1type emptyCtx struct{}2
3func (emptyCtx) Deadline() (deadline time.Time, ok bool) {4 return5}6
7func (emptyCtx) Done() <-chan struct{} {8 return nil9}10
11func (emptyCtx) Err() error {12 return nil13}14
15func (emptyCtx) Value(key any) any {2 collapsed lines
16 return nil17}
Thus, we can customize the Context
behaviors by creating a context hierarchy through the functions mentioned above.
1.3 Inherit
With the root Context, we can generate more sub-context using the functions below, which all take a parent
context and create a child context based on the parent context:
1func WithCancel(parent Context) (ctx Context, cancel CancelFunc)2func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)3func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)4func WithValue(parent Context, key, val interface{}) Context
1) WithCancel
The returned context’s Done channel is closed when:
- the returned cancel function is called or
- when the parent context’s Done channel is closed
whichever happens first.
1func WithCancelExample() {2 increment := func(ctx context.Context) <-chan int {3 dst := make(chan int)4 n := 05 go func() {6 for {7 select {8 case <-ctx.Done():9 return10 case dst <- n:11 n++12 }13 }14 }()15 return dst10 collapsed lines
16 }17 ctx, cancel := context.WithCancel(context.Background())18 defer cancel()19 for n := range increment(ctx) {20 fmt.Println(n)21 if n == 5 {22 break23 }24 }25}
1-2) WithCancelCause
Behave like WithCancel
, but returns a CancelCauseFunc
instead of a CancelFunc
. Calling cancel with a non-nil error recorrds that error in ctx
, which can then be retrieved using Cause(ctx)
.
1ctx, cancel := context.WithCancelCause(parent)2cancel(myError)3ctx.Err() // returns context.Canceled4context.Cause(ctx) // returns myError
2) WithDeadline
The returned [Context.Done] channel is closed when the deadline expires, when the returned cancel function is called, or when the parent context’s Done channel is closed, whichever happens first.
1func watch(ctx context.Context, name string) {2 for {3 select {4 case <-ctx.Done():5 fmt.Println(name, "stopped monitoring at", time.Now().Format("15:04:05"))6 return7 default:8 fmt.Println(name, "monitoring at", time.Now().Format("15:04:05"))9 time.Sleep(1 * time.Second)10 }11 }12}13
14func WithDeadlineExample() {15 ddl := time.Now().Add(3 * time.Second)13 collapsed lines
16 fmt.Println("Monitoring will be stopped at", ddl.Format("15:04:05"))17 ctx, cancel := context.WithDeadline(context.Background(), ddl)18 defer cancel()19 go watch(ctx, "Monitor1")20 time.Sleep(5 * time.Second)21}22
23// outputs:24// Monitoring will be stopped at 19:31:2025// Monitor1 monitoring at 19:31:1726// Monitor1 monitoring at 19:31:1827// Monitor1 monitoring at 19:31:1928// Monitor1 monitoring stopped at 19:31:20
3) WithTimeout
Similar to the example above:
1func WithTimeoutExample() {2 monitor := func(ctx context.Context, name string) {3 for {4 select {5 case <-ctx.Done():6 fmt.Println(name, "monitoring stopped at", time.Now().Format("15:04:05"))7 return8 default:9 fmt.Println(name, "monitoring at", time.Now().Format("15:04:05"))10 time.Sleep(1 * time.Second)11 }12 }13 }14 timeout := 3 * time.Second15 fmt.Println("Monitoring will be stopped after", timeout)12 collapsed lines
16 ctx, cancel := context.WithTimeout(context.Background(), timeout)17 defer cancel()18 go monitor(ctx, "Monitor1")19 time.Sleep(5 * time.Second)20}21
22// outputs:23// Monitoring will be stopped after 3s24// Monitor1 monitoring at 19:31:2225// Monitor1 monitoring at 19:31:2326// Monitor1 monitoring at 19:31:2427// Monitor1 monitoring stopped at 19:31:25
4) WithValue
Use context Values only for request-scoped data, not for passing optional params.
- Attention: the provided key must be:
- comparable
- should not be of type string or any other built-in type
1func monitor(ctx context.Context, mName MonitorName) {2 name := ctx.Value(mName)3 for {4 select {5 case <-ctx.Done():6 fmt.Println(name, "stopped monitoring at", time.Now().Format("15:04:05"))7 return8 default:9 fmt.Println(name, "monitoring at", time.Now().Format("15:04:05"))10 time.Sleep(1 * time.Second)11 }12 }13}14
15type MonitorName string14 collapsed lines
16
17func WithValueExample() {18 monitorName1 := MonitorName("MonitorKey1")19 ctx, cancel := context.WithCancel(context.Background())20 ctx = context.WithValue(ctx, monitorName1, "[Monitor1]")21 go monitor(ctx, monitorName1)22 time.Sleep(3 * time.Second)23 cancel()24}25
26// outputs:27// [Monitor1] monitoring at 19:46:1028// [Monitor1] monitoring at 19:46:1129// [Monitor1] monitoring at 19:46:12
5) Combined example
There is a 10s timeout for the monitor to be cancelled. And if we press ctrl+c on the keyboard within 10s, the mintor would also get cancelled.
1func MixedExample() {2 timeout := 10 * time.Second3 ctx, cancel := context.WithTimeout(context.Background(), timeout)4 defer cancel()5
6 cancelChan := make(chan os.Signal, 1)7 signal.Notify(cancelChan, os.Interrupt, syscall.SIGINT)8
9 go watch(ctx, "Monitor1")10 for {11 select {12 case <-cancelChan:13 cancel()14 fmt.Println("Cancel signal received and stop monitoring")15 return12 collapsed lines
16 default:17 time.Sleep(100 * time.Millisecond)18 }19 }20}21
22// outputs:23// Monitor1 monitoring at 19:59:0524// Monitor1 monitoring at 19:59:0625// Monitor1 monitoring at 19:59:0726// Monitor1 monitoring at 19:59:0827// ^CCancel signal received and stop monitoring
6) AfterFunc
-
Syntax:
func AfterFunc(ctx [Context](https://pkg.go.dev/context#Context), f func()) (stop func() bool)
-
Purpose: allows you to schedule a function to run after a context is done (either canceled or timed out)
-
Example:
1func main() {2ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)3defer cancel()45// Simulate a long-running task6go func() {7time.Sleep(10 * time.Second)8fmt.Println("Task completed")9}()1011// Schedule a cleanup function to run after the context is done12context.AfterFunc(ctx, func() {13fmt.Println("Cleaning up resources")14})153 collapsed lines16<-ctx.Done()17fmt.Println("Context done")18}
To see the full example of code, you can visit: goContext