Understanding and usage of context in Golang

It is Zion
7 min readDec 30, 2020

When a goroutine can start other goroutines, and those goroutines start other goroutines, and so on, then the first goroutine should be able to send cancellation signals to all embedded goroutines.

The sole purpose of the context package is to perform cancellation signals between goroutines, regardless of how they were generated.

The interface of the Context is defined as:

type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <- chan struct{}
Err() error
Value(key interface{}) interface{}
}
  • Deadline: The first value is the deadline, at which point the Context will automatically trigger the Cancel action. The second value is a boolean value, true means the deadline is set, false means the deadline is not set. If the deadline is not set, you have to call the cancel function manually to cancel the Context.
  • Done: return a read-only channel (only after being canceled), type struct{}, when this channel is readable, it means the parent context has initiated the cancel request, according to this signal, the developer can do some cleanup actions, exit the goroutine
  • Err: returns the reason why the context was cancelled
  • Value: returns the value bound to the Context, it is a key-value pair, so you need to pass a Key to get the corresponding value, this value is thread-safe

To create a Context, you must specify a parent Context.
Two built-in contexts, background and todo, serve as the top-level parent context:

var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}

Background, mainly used in the main function, initialization and test code, is the top-level Context of the tree structure, the root Context, which cannot be cancelled.
TODO, when you don’t know what Context to use, you can use this.

They are both essentially of type emptyCtx, both are non-cancelable, neither has a set deadline, and neither carries any value for the Context:

type emptyCtx intfunc (_ *emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (_ *emptyCtx) Done() <- chan struct{} {
return nil
}
func (_ *emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}

The context package also has several common functions:

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

Note that these methods mean that the context can be inherited once to achieve one more function, for example, using the WithCancel function to pass in the root context, it creates a child context, which has an additional function of cancel context, then use this context(context01) as the parent context, and pass it as the first parameter to the WithDeadline function, the child context(context02) is obtained, compared to the child context(context01), It has an additional function to cancel the context automatically after the deadline.

WithCancel

For channel, although channel can also notify many nested goroutines to exit, channel is not thread-safe, while context is thread-safe.

For example:

package mainimport (
"runtime"
"fmt"
"time"
"context"
)
func monitor2(ch chan bool, index int) {
for {
select {
case v := <- ch:
fmt.Printf("monitor2: %v, the received channel value is: %v, ending\n", index, v)
return
default:
fmt.Printf("monitor2: %v in progress...\n", index)
time.Sleep(2 * time.Second)
}
}
}
func monitor1(ch chan bool, index int) {
for {
go monitor2(ch, index)
select {
case v := <- ch:
// this branch is only reached when the ch channel is closed, or when data is sent(either true or false)
fmt.Printf("monitor1: %v, the received channel value is: %v, ending\n", index, v)
return
default:
fmt.Printf("monitor1: %v in progress...\n", index)
time.Sleep(2 * time.Second)
}
}
}
func main() {
var stopSingal chan bool = make(chan bool, 0)
for i := 1; i <= 5; i = i + 1 {
go monitor1(stopSingal, i)
}
time.Sleep(1 * time.Second) // close all gourtines
close(stopSingal)
// waiting 10 seconds, if the screen does not display <monitorX: xxxx in progress...>, all goroutines have been shut down
time.Sleep(10 * time.Second)
println(runtime.NumGoroutine())
println("main program exit!!!!")
}

The result of the execution is :

monitor1: 5 in progress...
monitor2: 5 in progress...
monitor1: 2 in progress...
monitor2: 2 in progress...
monitor2: 1 in progress...
monitor1: 1 in progress...
monitor1: 4 in progress...
monitor1: 3 in progress...
monitor2: 4 in progress...
monitor2: 3 in progress...
monitor1: 4, the received channel value is: false, ending
monitor1: 3, the received channel value is: false, ending
monitor2: 2, the received channel value is: false, ending
monitor2: 1, the received channel value is: false, ending
monitor1: 1, the received channel value is: false, ending
monitor2: 5, the received channel value is: false, ending
monitor2: 3, the received channel value is: false, ending
monitor2: 3, the received channel value is: false, ending
monitor2: 4, the received channel value is: false, ending
monitor2: 5, the received channel value is: false, ending
monitor2: 1, the received channel value is: false, ending
monitor1: 5, the received channel value is: false, ending
monitor1: 2, the received channel value is: false, ending
monitor2: 2, the received channel value is: false, ending
monitor2: 4, the received channel value is: false, ending
1
main program exit!!!!

Here a channel is used to send end notifications to all goroutines, but the situation here is relatively simple, if in a complex project, suppose multiple goroutines have some kind of error and execute repeatedly, then it is possible to repeatedly close the channel or close the channel and then write values to it, thus triggering a runtime panic. This is why we use context to avoid these problems, using WithCancel as an example:

package mainimport (
"runtime"
"fmt"
"time"
"context"
)
func monitor2(ctx context.Context, number int) {
for {
select {
case v := <- ctx.Done():
fmt.Printf("monitor: %v, the received channel value is: %v, ending\n", number,v)
return
default:
fmt.Printf("monitor: %v in progress...\n", number)
time.Sleep(2 * time.Second)
}
}
}
func monitor1(ctx context.Context, number int) {
for {
go monitor2(ctx, number)
select {
case v := <- ctx.Done():
// this branch is only reached when the ch channel is closed, or when data is sent(either true or false)
fmt.Printf("monitor: %v, the received channel value is: %v, ending\n", number, v)
return
default:
fmt.Printf("monitor: %v in progress...\n", number)
time.Sleep(2 * time.Second)
}
}
}
func main() {
var ctx context.Context = nil
var cancel context.CancelFunc = nil
ctx, cancel = context.WithCancel(context.Background())
for i := 1; i <= 5; i = i + 1 {
go monitor1(ctx, i)
}
time.Sleep(1 * time.Second) // close all gourtines
cancel()
// waiting 10 seconds, if the screen does not display <monitor: xxxx in progress>, all goroutines have been shut down
time.Sleep(10 * time.Second)
println(runtime.NumGoroutine())
println("main program exit!!!!")
}

WithTimeout and WithDeadline

WithTimeout and WithDeadline are basically the same in terms of usage and function, they both indicate that the context will be automatically canceled after a certain time, the only difference can be seen from the definition of the function, the second parameter passed to WithDeadline is of type time.Duration type, which is a relative time, meaning how long after the timeout is cancelled.

Example:

package mainimport (
"runtime"
"fmt"
"time"
"context"
)
func monitor2(ctx context.Context, index int) {
for {
select {
case v := <- ctx.Done():
fmt.Printf("monitor2: %v, the received channel value is: %v, ending\n", index, v)
return
default:
fmt.Printf("monitor2: %v in progress...\n", index)
time.Sleep(2 * time.Second)
}
}
}
func monitor1(ctx context.Context, index int) {
for {
go monitor2(ctx, index)
select {
case v := <- ctx.Done():
// this branch is only reached when the ch channel is closed, or when data is sent(either true or false)
fmt.Printf("monitor1: %v, the received channel value is: %v, ending\n", index, v)
return
default:
fmt.Printf("monitor1: %v in progress...\n", index)
time.Sleep(2 * time.Second)
}
}
}
func main() {
var ctx01 context.Context = nil
var ctx02 context.Context = nil
var cancel context.CancelFunc = nil
ctx01, cancel = context.WithCancel(context.Background())
ctx02, cancel = context.WithDeadline(ctx01, time.Now().Add(1 * time.Second)) // If it's WithTimeout, just change this line to "ctx02, cancel = context.WithTimeout(ctx01, 1 * time.Second)"
defer cancel() for i := 1; i <= 5; i = i + 1 {
go monitor1(ctx02, i)
}
time.Sleep(5 * time.Second) if ctx02.Err() != nil {
fmt.Println("the cause of cancel is: ", ctx02.Err())
}
println(runtime.NumGoroutine())
println("main program exit!!!!")
}

WithValue

Some required metadata can also be passed through the Context, which is appended to the Context for use.
Metadata is passed in as Key-Value, but note that the Key must be comparable and the Value must be thread-safe.

package mainimport (
"runtime"
"fmt"
"time"
"context"
)
func monitor(ctx context.Context, index int) {
for {
select {
case <- ctx.Done():
// this branch is only reached when the ch channel is closed, or when data is sent(either true or false)
fmt.Printf("monitor %v, end of monitoring. \n", index)
return
default:
var value interface{} = ctx.Value("Nets")
fmt.Printf("monitor %v, is monitoring %v\n", index, value)
time.Sleep(2 * time.Second)
}
}
}
func main() {
var ctx01 context.Context = nil
var ctx02 context.Context = nil
var cancel context.CancelFunc = nil
ctx01, cancel = context.WithCancel(context.Background())
ctx02, cancel = context.WithTimeout(ctx01, 1 * time.Second)
var ctx03 context.Context = context.WithValue(ctx02, "Nets", "Champion") // key: "Nets", value: "Champion"

defer cancel()
for i := 1; i <= 5; i = i + 1 {
go monitor(ctx03, i)
}
time.Sleep(5 * time.Second) if ctx02.Err() != nil {
fmt.Println("the cause of cancel is: ", ctx02.Err())
}
println(runtime.NumGoroutine())
println("main program exit!!!!")
}

There are also some notes about context:

  • Do not store Contexts in struct types, but pass the Context explicitly to each function that needs it, and the Context should be the first argument.
  • Do not pass a nil Context, even if the function allows it, or if you are not sure which Context to use, pass context.
  • Do not pass variables that could be passed as function arguments to the Value of the Context.

--

--