Detailed explanation of the function of context in golang

Time:2022-5-23

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

The only purpose of context packages is to perform cancellation signals between goroutines, regardless of how they are 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 this time, the context will automatically trigger the “Cancel” operation. The second value is Boolean. True indicates that the deadline is set, and false indicates that the deadline is not set. If no deadline is set, you must manually call the cancel function to cancel the context.
  • Done: return a read-only channel (only after cancellation). Type struct {}. When the channel is readable, it indicates that the parent context has initiated a cancellation request. According to this signal, developers can perform some cleanup operations and exit goroutine
  • Err: returns the reason for canceling the context
  • Value: returns the value bound to the context. It is a key value pair. Therefore, you need to pass a key to obtain the corresponding value. This value is thread safe

To create a context, you must specify a parent context. Two built-in contexts (background and to-do) are used as top-level parent contexts:


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

Background, mainly ü in the main function, the sed of initialization and test code, is the root context in the tree structure, which is the top-level context that cannot be cancelled. Todo, which you can use when you don’t know what context to use. They are essentially of the type emptyctx, which can not be cancelled. There is no fixed term and no value assigned to the context: type emptyctx int


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
}

Context packs also have several common functions: func withcancel (CTX context, cancelfunction) func withdeadline (parent context, deadline. Time) (context, cancelfunction) func withtimeout (parent context, timeout. Duration) (context, cancelfunction) func withvalue (parent context, key, Val interface {}) context

Please note that these methods mean that the context can be inherited at one time to realize other functions. For example, if you use the withcancel function to pass in the root context, it will create a sub context with the additional function of canceling the context, and then use this method to take the context (context01) as the parent context and pass it to the withdeadline function as the first parameter. Compared with the sub context (context01), Get the sub context (context02), which has an additional function to automatically cancel the context deadline later.

WithCancel

For the channel, although the channel can also notify many nested goroutines to exit, the channel is not thread safe, but the 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\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
 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 results of the implementation are:

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 an end notification to all goroutines, but the situation here is relatively simple. If in a complex project, assuming that multiple goroutines have some errors and execute repeatedly, the channel can be closed or closed repeatedly, and then the value can be written to it, so as to trigger the runtime panic. This is why we use context to avoid these problems. Take 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\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 usage and function. They both mean that the context will be automatically cancelled after a certain time. The only difference can be seen from the function definition. The second parameter passed to withdeadline is type time Duration type, which is a relative time and represents the time after canceling the timeout. 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\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 will be attached to the context for use. Metadata is passed as a key value, but note that the key must be comparable and the value must be thread safe.


package main
import (
 "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 considerations about context: do not store the context in the structure type, but explicitly pass the context to each function that needs it, and the context should be the first parameter.

Do not pass nil context even if the function allows, or if you are not sure which context to use, pass context. Do not pass variables that may be passed as function parameters to context values.

This is the end of this article about the role of context in golang. For more information about the role of context in golang, please search previous articles of developeppaer or continue to browse the relevant articles below. I hope you will support developeppaer in the future!