Jida's Blog

Golang Standard Package: context

4th December 2024
Golang
Golang Standard Package
Concurrency
Last updated:25th January 2025
7 Minutes
1247 Words

TL;TR

  • How to use Context:
    1. Avoid using empty Context, i.e. Background and TODO, directly
    2. Treat Context in params, and never put it in truct.
    3. As a param of the function, Context should always be the first param.
    4. The Value related function of Context should be used for passing only-needed data.
    5. Context is thread-safe and can be passed around in multiple goroutines.

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-Contexts of the three would also be notified, and thus be cleaned up and be released, which fixes the control issue of goroutine gracefully.

1
func 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
13
func 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
return
19
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

1
type Context interface {
2
Deadline() (deadline time.Time, ok bool)
3
Done() <-chan struct{}
4
Err() error
5
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. Once Done is readable, it means we recieves cancel signal.
  • Err:
    • If Done is not yet closed: returns nil.
    • If Done is closed: returns a non-nil error.
  • Value: returns the value associated with this context for key, or nil.

1.2 Create a root context

1
var (
2
background = new(emptyCtx)
3
todo = new(emptyCtx)
4
)
5
6
func Background() Context {
7
return background
8
}
9
10
func TODO() Context {
11
return todo
12
}

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:

  1. they cannot be cancelled
  2. does not have a deadline set
  3. does not carry any values
1
type emptyCtx struct{}
2
3
func (emptyCtx) Deadline() (deadline time.Time, ok bool) {
4
return
5
}
6
7
func (emptyCtx) Done() <-chan struct{} {
8
return nil
9
}
10
11
func (emptyCtx) Err() error {
12
return nil
13
}
14
15
func (emptyCtx) Value(key any) any {
2 collapsed lines
16
return nil
17
}

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:

1
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
2
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
3
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
4
func WithValue(parent Context, key, val interface{}) Context

1) WithCancel

The returned context’s Done channel is closed when:

  1. the returned cancel function is called or
  2. when the parent context’s Done channel is closed

whichever happens first.

1
func WithCancelExample() {
2
increment := func(ctx context.Context) <-chan int {
3
dst := make(chan int)
4
n := 0
5
go func() {
6
for {
7
select {
8
case <-ctx.Done():
9
return
10
case dst <- n:
11
n++
12
}
13
}
14
}()
15
return dst
10 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
break
23
}
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).

1
ctx, cancel := context.WithCancelCause(parent)
2
cancel(myError)
3
ctx.Err() // returns context.Canceled
4
context.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.

1
func 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
return
7
default:
8
fmt.Println(name, "monitoring at", time.Now().Format("15:04:05"))
9
time.Sleep(1 * time.Second)
10
}
11
}
12
}
13
14
func 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:20
25
// Monitor1 monitoring at 19:31:17
26
// Monitor1 monitoring at 19:31:18
27
// Monitor1 monitoring at 19:31:19
28
// Monitor1 monitoring stopped at 19:31:20

3) WithTimeout

Similar to the example above:

1
func 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
return
8
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.Second
15
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 3s
24
// Monitor1 monitoring at 19:31:22
25
// Monitor1 monitoring at 19:31:23
26
// Monitor1 monitoring at 19:31:24
27
// 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:
    1. comparable
    2. should not be of type string or any other built-in type
    To avoid collisions between packages using context.
1
func 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
return
8
default:
9
fmt.Println(name, "monitoring at", time.Now().Format("15:04:05"))
10
time.Sleep(1 * time.Second)
11
}
12
}
13
}
14
15
type MonitorName string
14 collapsed lines
16
17
func 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:10
28
// [Monitor1] monitoring at 19:46:11
29
// [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.

1
func MixedExample() {
2
timeout := 10 * time.Second
3
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
return
12 collapsed lines
16
default:
17
time.Sleep(100 * time.Millisecond)
18
}
19
}
20
}
21
22
// outputs:
23
// Monitor1 monitoring at 19:59:05
24
// Monitor1 monitoring at 19:59:06
25
// Monitor1 monitoring at 19:59:07
26
// Monitor1 monitoring at 19:59:08
27
// ^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:

    1
    func main() {
    2
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    3
    defer cancel()
    4
    5
    // Simulate a long-running task
    6
    go func() {
    7
    time.Sleep(10 * time.Second)
    8
    fmt.Println("Task completed")
    9
    }()
    10
    11
    // Schedule a cleanup function to run after the context is done
    12
    context.AfterFunc(ctx, func() {
    13
    fmt.Println("Cleaning up resources")
    14
    })
    15
    3 collapsed lines
    16
    <-ctx.Done()
    17
    fmt.Println("Context done")
    18
    }

To see the full example of code, you can visit: goContext

References

Article title:Golang Standard Package: context
Article author:Jida-Li
Release time:4th December 2024