Understanding and usage of context in Golang
By: Will Chu
Tags: Programming Languages, Programming, Golang Tutorial, Computer Science, Golang

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 returned is the deadline, at which point the Context will automatically trigger the Cancel action, the second value returned 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 manually call the If the deadline is not set, you have to manually call the cancel function 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 int func (_ *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 main import (

"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

", index, v)

return

default:

fmt.Printf("monitor2: %v in progress...

", 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

", index, v)

return

default:

fmt.Printf("monitor1: %v in progress...

", 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

cancel() // 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 main import (

"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

", number,v)

return

default:

fmt.Printf("monitor: %v in progress...

", 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

", number, v)

return

default:

fmt.Printf("monitor: %v in progress...

", 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 main import (

"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

", index, v)

return

default:

fmt.Printf("monitor2: %v in progress...

", 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

", index, v)

return

default:

fmt.Printf("monitor1: %v in progress...

", 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.