Context You Should Know in Golang

Exploring Context Package in Go: A Comprehensive Guide

Listen to what you know instead of what you fear.
— Richard Bach

Medium Link: Context Package You Should Know in Golang | by Wesley Wei | Jun, 2024 | Programmer’s Career

Hello, here is Wesley, Today’s article is about golang-context in Go. Without further ado, let’s get started.💪

1.1 Context Introduction

From the versions of the context package on context package versions - context - Go Packages, we can see that Go’s standard library introduced the context package in version go1.7beta1. It defines the Context type, which represents the context of a goroutine, can be used to transmit contextual information, such as cancellation signals, timeouts, deadlines, and key-value pairs.

We can understand it as a container for the context in which a program runs. Of course, this is just some basic understanding, and the Go documentation has a more critical statement:

The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines.

This statement suggests a close relationship between context and goroutines. Let’s continue to explore further.

1.2 Why Context

As we all know, a seemingly simple business request in the server-side is actually very complex. Multiple goroutines are working simultaneously to complete it. Some go to Redis to retrieve metadata, some go to S3 to retrieve specific data, and some go to call the interfaces of downstream microservices.

This requires that requests have a hierarchical relationship, making it easy to perform timeouts and resource recovery, for example, we do not want the downstream goroutines to continue executing tasks when the upstream request stops, as this will waste resources, resource leakage, or even service avalanche.

In Go’s unique design, context is introduced to solve this kind of problem (which is rarely seen in other programming languages).

image.png

Through context and goroutines’ cooperation, we can achieve the effect introduced in Go Concurrency Patterns: Context - The Go Programming Language in the introduction.

When a request is canceled or times out, all the goroutines working on that request should exit quickly so the system can reclaim any resources they are using.

As for how to implement it, we can find the answer in the source code analysis.

1.3 Creating and Using Context

We can create a context using the functions provided by the context package:

1. context.Background() and context.TODO()

context.Background() returns an empty context that does not contain any data, does not support cancellation operations, and does not have a deadline set. We typically use context.Background() in the main function, initialization, and testing code.

On the other hand, context.TODO() returns an empty context as well, but in terms of semantics, we choose to use context.TODO() when we are unsure which context to use.

1
2
ctx := context.Background()
ctx = context.TODO()

WithCancel Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
"context"
"fmt"
)

func main() {
// gen generates integers in a separate goroutine and
// sends them to the returned channel.
// The callers of gen need to cancel the context once
// they are done consuming generated integers not to leak
// the internal goroutine started by gen.
gen := func(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <-ctx.Done():
return // returning not to leak the goroutine
case dst <- n:
n++
}
}
}()
return dst
}

ctx, cancel := context.WithCancel(context.Background())
defer cancel() // cancel when we are finished consuming integers

for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
break
}
}
}

// run output
1
2
3
4
5

WithDeadline, WithTimeout, WithValue, etc. Can be used as shown in the official examples Context Examples.

1.4 Context Source Code Analysis

The focus here is to analyze the example of WithCancel in go1.22.4, specifically in the WithCancel function.

1
2
3
4
5
6
7
8
9
10
11
12
13
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := withCancel(parent)
return c, func() { c.cancel(true, Canceled, nil) }
}

func withCancel(parent Context) *cancelCtx {
if parent == nil {
panic("cannot create context from nil parent")
}
c := &cancelCtx{}
c.propagateCancel(parent, c)
return c
}

We can break down the execution steps as follows:

  • Create a cancelCtx object, which serves as the child context
  • Then call propagateCancel to establish the relationship between the parent and child contexts. This way, when the parent context is canceled, the child context will also be canceled.
  • Return the child context object and the cancel function for the sub-tree.

cancelCtx and propagateCancel()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
type cancelCtx struct {
Context

mu sync.Mutex // protects following fields
done atomic.Value // of chan struct{}, created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
cause error // set to non-nil by the first cancel call
}

func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
c.Context = parent

done := parent.Done()
if done == nil {
return // parent is never canceled
}

select {
case <-done:
// parent is already canceled
child.cancel(false, parent.Err(), Cause(parent))
return
default:
}

if p, ok := parentCancelCtx(parent); ok {
// parent is a *cancelCtx, or derives from one.
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err, p.cause)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
return
}

if a, ok := parent.(afterFuncer); ok {
// parent implements an AfterFunc method.
c.mu.Lock()
stop := a.AfterFunc(func() {
child.cancel(false, parent.Err(), Cause(parent))
})
c.Context = stopCtx{
Context: parent,
stop: stop,
}
c.mu.Unlock()
return
}

goroutines.Add(1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err(), Cause(parent))
case <-child.Done():
}
}()
}

The following function has three different cases related to the parent context:

  1. When parent.Done() == nil, or in other words, parent will not trigger the cancel event, the current function will simply return;
  2. When the child context’s inheritance chain contains a cancellable context, it will check if parent has already triggered the cancel signal;
    • If already canceled, the child will be immediately canceled;
    • If not canceled, the child will be added to parent‘s children list, waiting for parent to release the cancel signal;
  3. When the parent context is a developer-defined type, implements the context.Context interface, and returns a non-empty channel in the Done() method;
    1. Run a new Goroutine simultaneously monitoring parent.Done() and child.Done() Channels;
    2. In parent.Done() close, call child.cancel to cancel the child context;

The purpose of context.propagateCancel is to synchronize cancel and end signals between parent and child, ensuring that when parent is canceled, the child also receives the corresponding signal, avoiding inconsistent states.

cancel()

The most important method is context.cancelCtx.cancel, which closes the Channel in the context and synchronizes the cancel signal to all child contexts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
if cause == nil {
cause = err
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
c.cause = cause
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
} else {
close(d)
}
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err, cause)
}
c.children = nil
c.mu.Unlock()

if removeFromParent {
removeChild(c.Context, c)
}
}

From the source code, we can see that the cancel method can be called multiple times and is idempotent. In summary, we can create a simple diagram to illustrate the cancel process:

image.png

Other Methods

context.WithDeadline and context.WithTimeout have similar logic but also handle timers. They create a context.timerCtx during the creation process and check the deadline date of the parent context against the current date. They then create a timer using time.AfterFunc, and when the time exceeds the deadline, they call context.timerCtx.cancel to synchronize the cancel signal.

XXXCause further extends the error-related process, solving some annoying problems such as unfriendly error returns, inability to customize errors, and difficulty debugging when encountering issues that are hard to trace.

1
2
3
4
ctx, cancel := context.WithCancelCause(parent)
cancel(myError)
ctx.Err() // returns context.Canceled
context.Cause(ctx) // returns myError

For more details, refer to the following article: Context cancel cause in Go 1.20. Go 1.20 introduced two new functions in… | by Peter Gillich | Dev Genius

1.5 Best Practices for context

Here are some best practices to consider:

  1. Do not store context in structs: Context was designed to facilitate passing between functions, not to store state information. Therefore, it’s best to maintain its fluidity rather than storing it in structs.
  2. Use context’s Value method sparingly: Although the Value method of context is very useful in some scenarios, overuse of this feature can make the code harder to maintain, especially when context is heavily used.
  3. Follow the lifecycle management rules of the context package: Generally, context should be created by the outermost function that initiates the request and passed down through the call chain. If your function needs to run in a subprocess and may need to be canceled or timed out, consider receiving a context parameter. Avoid storing and using context in global variables.
  4. Call cancel functions using defer statements: When using the WithCancel, WithTimeout, or WithDeadline functions to return cancel functions, you should call them to release related resources. The best practice is to call these cancel functions through defer statements before your function returns.
  5. Create new contexts as much as possible: Instead of reusing existing contexts, this allows each request to have a context with a clear lifecycle.

1.6 Summary

The Go official recommendation is to use Context as the first parameter of functions. If we want to control the cancellation actions of all coroutines, almost all functions should have a Context parameter, and context will spread like a virus everywhere.

Additionally, functions like WithCancel actually create a series of linked list nodes. We know that operations on linked lists usually have O(n) complexity, and the more child nodes there are, the lower the efficiency will be.

So, what problem does the context package solve? The answer is: cancelation. It helps us better manage parallel control and better manage goroutine overuse. Although it is not perfect, it does solve the problem in a simple way.


More Series Articles about You Should Know In Golang:

https://wesley-wei.medium.com/list/you-should-know-in-golang-e9491363cd9a

And I’m Wesley, delighted to share knowledge from the world of programming. don’t forget to follow me, it would be a great help to me. See you in the next article. 👋

中文文章: https://programmerscareer.com/zh-cn/golang-context/
Author: Wesley Wei – Twitter Wesley Wei – Medium
Note: Originally written at https://programmerscareer.com/golang-context/ at 2024-06-13 01:12. If you choose to repost or use this article, please cite the original source.

Switching Between Different Programming Languages Quickly: VSCode Profiles Nil Notions You Should Know in Golang

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×