SoFunction
Updated on 2025-03-04

Detailed explanation of the role of context in golang

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 a cancel signal to all other goroutines.

The sole purpose of context packages is to perform cancel signals between goroutines regardless of how they are generated. The context interface is defined as:

type Context interface {
 Deadline() (deadline , ok bool)
 Done() <- chan struct{}
 Err() error
 Value(key interface{}) interface{}
}
  • Deadline: The first value is the expiration date, and the context will automatically trigger the "Cancel" operation. The second value is a boolean value, true means that the deadline has been set, and false means that the deadline has not been set. If no deadline is set, the cancel function must be called manually to cancel the context.
  • Done: Return a read-only channel (only after cancellation), type struct {}, when the channel is readable, it means that the parent context has initiated a cancel request. According to this signal, the developer can perform some clearing operations and exit goroutine
  • Err: Return the reason for canceling the context
  • 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, which is thread-safe

To create a context, the parent context must be specified. 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, initialization and test code sed, is the tree structure, the root context, which cannot be cancelled. TODO, you can use it when you don't know what context to use. They are essentially emptyCtx types, they are all non-cancellable, have no fixed period, and do not assign any value to the Context: Type emptyCtx int

type emptyCtx int
func (_ *emptyCtx) Deadline() (deadline , 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) (context, CancelFunc) func WithTimeout (parent context, timeout. Duration) (context, CancelFunc) func WithValue (parent context, key, val interface{}) context

Note that these methods mean that the context can be inherited at once to implement other functions, for example, passing into the root context using the WithCancel function, it creates a child context with the additional function of canceling the context, then using this method to use context(context01) as the parent context and pass it as the first parameter to the WithDeadline function, obtaining the child context(context02) compared to the child context(context01), which has an additional function that automatically cancels the context deadline afterwards.

WithCancel

For channels, although channels can also notify many nested goroutines to exit, channels are not thread-safe and context are thread-safe.

For example:

package main
import (
 "runtime"
 "fmt"
 "time"
 "context"
)
func monitor2(ch chan bool, index int) {
 for {
  select {
  case v := <- ch:
   ("monitor2: %v, the received channel value is: %v, ending\n", index, v)
   return
  default:
   ("monitor2: %v in progress...\n", index)
   (2 * )
  }
 }
}
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)
   ("monitor1: %v, the received channel value is: %v, ending\n", index, v)
   return
  default:
   ("monitor1: %v in progress...\n", index)
   (2 * )
  }
 }
}
func main() {
 var stopSingal chan bool = make(chan bool, 0)
 for i := 1; i <= 5; i = i + 1 {
  go monitor1(stopSingal, i)
 }
 (1 * )
 // close all gourtines
 cancel()
 // waiting 10 seconds, if the screen does not display <monitorX: xxxx in progress...>, all goroutines have been shut down
 (10 * )
 println(())
 println("main program exit!!!!")
}

The result of 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 an end notification to all goroutines, but the situation here is relatively simple. If in a complex project, assuming that multiple goroutines have some error and are executed repeatedly, you can repeatedly close or close the channel channel and then write a value to it, triggering runtime panic. This is why we use context to avoid these problems, taking WithCancel as an example:

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

WithTimeout and WithDeadline

WithTimeout and WithDeadline are basically the same in usage and functions. They both indicate that the context will be automatically cancelled after a certain time. The only difference can be seen from the definition of the function. The second parameter passed to WithDeadline is the type type, which is a relative time, indicating the time after the cancellation timeout. example:

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

WithValue

Some required metadata can also be passed through a context that will be appended to the context for use. Metadata is passed as key values, but be aware that the key must be comparable and the values ​​must be thread-safe.

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

 defer cancel()
 for i := 1; i <= 5; i = i + 1 {
  go monitor(ctx03, i)
 }
 (5 * )
 if () != nil {
  ("the cause of cancel is: ", ())
 }
 println(())
 println("main program exit!!!!")
}

There are some notes about context: don't store the Context in the struct type, but pass the Context explicitly to every function that needs it, and the Context should be the first parameter.

Don't pass nil Context even if the function allows it, or if you're 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 related content on the role of context in golang, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!