Preface
In daily Go development, the Context package is the most commonly used one. Almost all functions have the first parameter ctx. So why do we pass Context? What are the usages of Context and what is the underlying implementation? I believe you will definitely have the desire to explore, so let’s follow this article and learn together!
Need one
In development, other functions will definitely be called, such as A calls B. The timeout time will often be set during the call process. For example, if it exceeds 2s, it will not wait for the result of B and will return directly. So what do we need to do?
// Sleep for 5 seconds, simulate long-term operationfunc FuncB() (interface{}, error) { (5 * ) return struct{}{}, nil } func FuncA() (interface{}, error) { var res interface{} var err error ch := make(chan interface{}) // Call FuncB() and save the result to channel go func() { res, err = FuncB() ch <- res }() // Set a 2s timer timer := (2 * ) // Is the monitoring timer ending first, or is FuncB returning the result first select { // Timeout, return the default value case <-: return "default", err // FuncB returns the result first, close the timer, and return the result of FuncB. case r := <-ch: if !() { <- } return r, err } } func main() { res, err := FuncA() (res, err) }
Our implementation above can realize that after exceeding the waiting time, A does not wait for B, but B does not feel the cancel signal. If B is a calculation density function, we also hope that B will perceive the cancel signal, cancel the calculation in time and return to reduce resource waste.
In another case, if there are multiple layers of calls, such as A calls B, C, B calls D, E, C calls E, F, after the timeout time exceeds A, we hope that the cancel signal can be passed layer by layer, and all subsequent calls can be sensed and returned in time.
Need two
When calling multiple layers, A->B->C->D, some data needs to be transmitted fixedly, such as LogID. By printing the same LogID, we can trace a call to facilitate problem detection. If you need to pass parameters every time, it would be too troublesome. We can use Context to save it. By setting a fixed key, the value is taken out as the LogID when printing the log.
const LogKey = "LogKey" // Simulate a log printing, and each time the LogKey corresponding value is taken out as LogID from the Context as the LogIDtype Logger struct{} func (logger *Logger) info(ctx , msg string) { logId, ok := (LogKey).(string) if !ok { logId = ().String() } (logId + " " + msg) } var logger Logger // Log printing and call FuncBfunc FuncA(ctx ) { (ctx, "FuncA") FuncB(ctx) } func FuncB(ctx ) { (ctx, "FuncB") } // Get the initialized Context with LogID, usually done in the program entrancefunc getLogCtx(ctx ) { logId, ok := (LogKey).(string) if ok { return ctx } logId = () return (ctx, LogKey, logId) } func main() { ctx = getLogCtx(()) FuncA(ctx) }
This uses the valueCtx mentioned in this article. Continue reading and learn how valueCtx is implemented!
Context interface
type Context interface { Deadline() (deadline , ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
The Context interface is relatively simple and defines four methods:
- The Deadline() method returns two values. deadline indicates at what time the Context will be cancelled, and ok indicates whether deadline is set. When ok=false, it means that deadline is not set, then deadline will be a zero value at this time. Calling this method multiple times returns the same result.
- Done() returns a read-only channel of type chan struct{}. If the current Context does not support cancellation, Done returns nil. We know that if there is no data in a channel, reading the data will block; if the channel is closed, the data can be read, so you can listen to the channel returned by Done to get the signal that the Context is cancelled.
- Err() returns the reason why the channel returned by Done is closed. When the channel is not closed, Err() returns nil; when the channel is closed, it returns the corresponding value, such as Canceled and DeadlineExceeded. After Err() returns a non-nil value, the next call will return the same value.
- Value() returns the value of the key value pair saved by Context, and returns nil if key does not exist.
Done() is a relatively common method. The following is an example of a classic streaming task: Listen to whether () is closed to determine whether the task needs to be cancelled. If it needs to be canceled, the corresponding reason will be returned; if it does not cancel, the result of the calculation will be written into the out channel.
func Stream(ctx , out chan<- Value) error { for { // Process data v, err := DoSomething(ctx) if err != nil { return err } //() Read the data, indicating that the task cancellation signal has been obtained select { case <-(): return () // Otherwise, the result will be output and the calculation will continue case out <- v: } } }
Value() is also a relatively common method for passing some data in the context. Use the () method to save the key and value, and you can get the value according to the key through the Value() method.
func main() { ctx := () c := (ctx, "key", "value") v, ok := ("key").(string) (v, ok) }
emptyCtx
The Context interface does not require us to implement it manually. Generally, we directly use the Background() method and TODO() method provided in the context package to obtain the most basic Context.
var ( background = new(emptyCtx) todo = new(emptyCtx) ) func Background() Context { return background } func TODO() Context { return todo }
The Background() method is generally used in main functions or program initialization methods; TODO() can be used when we don't know which Context to use, or if the Context is not passed above.
Background() and TODO() are both generated based on emptyCtx. As can be seen from the name, emptyCtx is an empty Context, without deadline, cannot be cancelled, and there are no key-value pairs.
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 } func (e *emptyCtx) String() string { switch e { case background: return "" case todo: return "" } return "unknown empty Context" }
In addition to the two most basic Contexts above, the context package provides more functional Contexts, including valueCtx, cancelCtx, and timerCtx. Let's take a look at them one by one.
valueCtx
Example of usage
We generally use the () method to store key-value pairs into the Context, and then use the Value() method to obtain value based on key. The implementation of this function depends on valueCtx.
func main() { ctx := () c := (ctx, "myKey", "myValue") v1 := ("myKey") (v1.(string)) v2 := ("hello") (v2) // nil }
Type definition
The valueCtx structure has a Context nested, and uses key and value to save key-value pairs:
type valueCtx struct { Context key, val interface{} }
WithValue
The context package exposes the WithValue method to create a valueCtx based on a parent context. As can be seen from the source code below, the key must be comparable!
func WithValue(parent Context, key, val interface{}) Context { if parent == nil { panic("cannot create context from nil parent") } if key == nil { panic("nil key") } if !(key).Comparable() { panic("key is not comparable") } return &valueCtx{parent, key, val} }
*valueCtx implements Value(), and you can get value based on key. This is a process of upward recursive search. If the key is not in the current valueCtx, it will continue to look for parent Context upward until the top level of the Context is found. Generally, the top level of emptyCtx is empty, and () returns nil.
func (c *valueCtx) Value(key interface{}) interface{} { if == key { return } return (key) }
cancelCtx
cancelCtx is a Context used to cancel a task. The task determines whether to continue processing the task or return directly by listening to whether the Context is cancelled.
In the following example, we define a cancelCtx in the main function and call cancel() after 2s to cancel the Context, that is, we hope doSomething() will complete the task within 2s, otherwise we can return directly, and there is no need to continue to calculate and waste resources.
Inside the doSomething() method, we use select to listen to whether the task is completed, and whether the Context has been cancelled, and which branch will be executed as soon as it arrives first. The method simulates a 5s task. The main function wait time is 2s, so the task is not completed; if the main function wait time is changed to 10s, the task is completed and the result will be returned.
This is just a one-layer call. In real cases, there may be multiple levels of calls. For example, doSomething may call other tasks. Once the parent Context is cancelled, all subsequent tasks should be cancelled.
func doSomething(ctx ) (interface{}, error) { res := make(chan interface{}) go func() { ("do something") ( * 5) res <- "done" }() select { case <-(): return nil, () case value := <-res: return value, nil } } func main() { ctx, cancel := (()) go func() { ( * 2) cancel() }() res, err := doSomething(ctx) (res, err) // nil , context canceled }
Next, let's study how cancelCtx is cancelled
Type definition
- The canceler interface contains cancel() and Done() methods, and both *cancelCtx and *timerCtx implement this interface.
- closedchan is a closed channel that can be used after Done() returns
- canceled is an err for the reason why Context is cancelled
type canceler interface { cancel(removeFromParent bool, err error) Done() <-chan struct{} } // closedchan is a reusable closed channel. var closedchan = make(chan struct{}) func init() { close(closedchan) } var Canceled = ("context canceled")
CancelFunc is a function type definition, a cancel function, with the following specifications:
- CancelFunc tells a task to stop working
- CancelFunc will not wait for the task to end
- CancelFunc supports concurrent calls
- After the first call, subsequent calls will not produce any effect.
type CancelFunc func()
&cancelCtxKey is a fixed key that returns cancelCtx itself
var cancelCtxKey int
cancelCtx
cancelCtx can be cancelled, it nests the Context interface and implements the canceler interface. cancelCtx uses the children field to save children that also implement the canceler interface. When cancelCtx is cancelled, all children will also be cancelled.
type cancelCtx struct { Context mu // Protect the following fields to ensure thread safety done // Save the channel, lazy loading, this channel will be closed when calling the cancel method. children map[canceler]struct{} // Save child nodes, and will be set to nil when the cancel method is called the first time. err error // Why is the save cancel? The default is nil. The first call to cancel will be assigned a value.}
The Value() method of *cancelCtx is similar to the Value() method of *valueCtx, except that a fixed key is added: &cancelCtxKey. Returns itself when the key is &cancelCtxKey
func (c *cancelCtx) Value(key interface{}) interface{} { if key == &cancelCtxKey { return c } return (key) }
The done field of *cancelCtx is lazy to load and will only be assigned when the Done() method or cancel() is called.
func (c *cancelCtx) Done() <-chan struct{} { d := () // If there is already a value, return directly if d != nil { return d.(chan struct{}) } // No value, add lock assignment () defer () d = () if d == nil { d = make(chan struct{}) (d) } return d.(chan struct{}) }
The Err method returns the err field of cancelCtx
func (c *cancelCtx) Err() error { () err := () return err }
WithCancel
So how do we create a cancelCtx? The context package provides the WithCancel() method, allowing us to create a cancelCtx based on a Context. The WithCancel() method returns two fields, one is cancelCtx generated based on the incoming Context and the other is CancelFunc.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { if parent == nil { panic("cannot create context from nil parent") } c := newCancelCtx(parent) propagateCancel(parent, &c) return &c, func() { (true, Canceled) } }
WithCancel calls two external methods: newCancelCtx and propagateCancel. newCancelCtx is relatively simple. According to the passed context, a cancelCtx structure is returned.
func newCancelCtx(parent Context) cancelCtx { return cancelCtx{Context: parent} }
propagateCancel can be seen from the name, which means spreading cancel. If the parent Context supports cancellation, then we need to establish a notification mechanism so that when the parent node is cancelled, the notification child nodes will also be cancelled and propagated layer by layer.
In propagateCancel, if the parent Context is of cancelCtx type and is not cancelled, the child Context will be hung under it to form a tree structure; in other cases, it will not be mounted.
func propagateCancel(parent Context, child canceler) { // If parent does not support cancellation, then cancel propagation is not supported, return directly done := () if done == nil { return } // � select { case <-done: // If parent has been cancelled at this time, then tell the child node to cancel it directly. (false, ()) return default: } // � // If parent is not cancelled cancelCtx if p, ok := parentCancelCtx(parent); ok { // Add lock to prevent concurrent updates () // Judge again, because it is possible that the previous one who obtained the lock was cancelled. // If parent has been cancelled, then the child node will also be cancelled directly. if != nil { (false, ) } else { // hang the child Context to the child field of parent cancelCtx // � if == nil { = make(map[canceler]struct{}) } [child] = struct{}{} } () } else { // parent is not a cancelCtx type, it may be a Context implemented by the user themselves atomic.AddInt32(&goroutines, +1) // Start a coroutine listening. If parent is cancelled, the subcontext is also cancelled. go func() { select { case <-(): (false, ()) case <-(): } }() } }
The cancel method is to cancel cancelCtx. The main job is: close the channel in , assign a value to err, and then cascadingly cancel all subcontexts. If removeFromParent is true, the tree with that node as the top of the tree is deleted from the parent node.
The cancel() method is only responsible for the scope of its own jurisdiction, that is, itself and its own child nodes, and then determines whether it is necessary to remove the tree that is vertex from the parent node according to the configuration. If the child node has child nodes, the child nodes are responsible for handling it and they don’t have to be responsible for it themselves.
There are three calls to the cancel() method in propagateCancel(), and the removedFromParent passed in is false because it was not mounted at the time and does not need to be removed. The CancelFunc returned by WithCancel, the incoming removeFromParent is true because calling propagateCancel may cause a mount. When a mount is generated, calling cancel() needs to be removed.
func (c *cancelCtx) cancel(removeFromParent bool, err error) { // err refers to the reason for cancellation, must be passed, cancelCtx is ("context canceled") if err == nil { panic("context: internal error: missing cancel error") } // When it comes to modifying the protection field value, locks are required () // If the Context has been cancelled, return directly. Call cancel multiple times without additional effects if != nil { () return } // Assign a value to err, here err must not be nil = err // close channel d, _ := ().(chan struct{}) // Because it is lazy loading, there may be a nil // If there is no value in , directly assign a closedchan; otherwise, directly close if d == nil { (closedchan) } else { close(d) } // traverse all child Contexts of the current cancelCtx, so that the child nodes can also cancel // Because the current Context will take the initiative to remove the child Context, the child Context does not need to actively escape from the parent // Therefore, the incoming removeFromParent is false for child := range { (false, err) } // Putting children empty is equivalent to removing all children of it = nil () // If the current cancelCtx needs to be removed from the upper cancelCtx, call the removeChild method // It's your father Context if removeFromParent { removeChild(, c) } }
From the propagateCancel method, we can see that only parent belongs to the cancelCtx type will mount itself. Therefore, removeChild will again determine whether parent is cancelCtx, which is consistent with the previous logic. If you find it, remove yourself. It should be noted that removing will remove all the child nodes below you and yourself.
If the propagateCancel method in the previous step mounts itself to A, but when cancel() is called, A has been cancelled, parentCancelCtx() will return false. But this doesn't matter. When A cancels, the mounted child node has been removed, and the current child node does not need to remove itself from A.
func removeChild(parent Context, child canceler) { // Parent Is it an uncancelCtx that is cancelCtx p, ok := parentCancelCtx(parent) if !ok { return } // Get the lock of parent cancelCtx and modify the protection field children () // Delete yourself from the child of parent cancelCtx if != nil { delete(, child) } () }
parentCancelCtx determines whether parent is *cancelCtx that has not been cancelled. It is easy to judge whether the cancellation is not allowed. What is difficult to judge is whether parent is *cancelCtx, because it is possible that other structures have cancelCtx embedded, such as timerCtx, which will be determined by comparing the channel.
func parentCancelCtx(parent Context) (*cancelCtx, bool) { // If the done of parent context is nil, it means that cancel is not supported, then it cannot be cancelCtx // If the done of parent context is closedchan, it means that parent context has canceled done := () if done == closedchan || done == nil { return nil, false } // It is explained here that cancellation is supported and has not been cancelled // If parent context belongs to the native *cancelCtx or derivative type, subsequent judgments need to be made. // If parent context cannot be converted to *cancelCtx, it is considered non-cancelCtx and returns nil, fasle p, ok := (&cancelCtxKey).(*cancelCtx) if !ok { return nil, false } // After the above judgment, it is stated that parent context can be converted to *cancelCtx, and there are many situations at this time: // - parent context is *cancelCtx // - parent context is the timerCtx in the standard library // - parent context is a custom package of cancelCtx // // The judgment method is: // Judge whether the done channel obtained by parent context through Done() method is consistent with the done channel of context found by Value . // // Consistent situation statement parent context is cancelCtx or timerCtx or custom cancelCtx and has not been rewritten Done(), // In this case, it can be considered that the bottom-level *cancelCtx has been obtained // // Inconsistency statement parent context is a custom cancelCtx and the Done() method is rewritten and does not return the standard *cancelCtx. // The done channel of , this situation needs to be handled separately, so return nil, false pdone, _ := ().(chan struct{}) if pdone != done { return nil, false } return p, true }
timerCtx
Introduction
timerCtx embeds cancelCtx and adds a timer and deadline field. The cancellation capability of timerCtx is to reuse cancelCtx, but a timed cancellation is added on this basis.
During our use process, it is possible that the task is completed in advance before the deadline has arrived. At this time, CancelFunc needs to be called manually.
func slowOperationWithTimeout(ctx ) (Result, error) { ctx, cancel := (ctx, 100*) defer cancel() // If the deadline is not reached, slowOperation will be completed. Call cancel() as soon as possible to release the resource return slowOperation(ctx) }
Type definition
type timerCtx struct { cancelCtx // Inline cancelCtx timer * // Protected by mutex lock deadline // Deadline}
Deadline() Returns the value of the deadline field
func (c *timerCtx) Deadline() (deadline , ok bool) { return , true }
WithDeadline
WithDeadline returns a timed cancellation Context and a CancelFunc based on parent Context and point-time d. There are three cases where the returned Context is cancelled: 1. When the specified time is reached, it will be cancelled automatically; 2. Manually call CancelFunc; 3. The parent Context is cancelled, causing the Context to be cancelled. Which of these three situations comes first, the cancellation operation will be triggered for the first time, and subsequent cancellation will not have any effect.
If the deadline passed into parent Context is earlier than the specified time d, d is useless at this time, and you can just rely on parent to cancel the propagation.
func WithDeadline(parent Context, d ) (Context, CancelFunc) { // The parent passed in cannot be nil if parent == nil { panic("cannot create context from nil parent") } // parent also has deadline, and it is earlier than d. Just rely on parent to cancel the propagation directly if cur, ok := (); ok && (d) { // The current deadline is already sooner than the new one. return WithCancel(parent) } // Define the timerCtx interface c := &timerCtx{ cancelCtx: newCancelCtx(parent), deadline: d, } // Set propagation. If parent belongs to cancelCtx, it will be mounted to the children field propagateCancel(parent, c) // How long is it to the deadline d dur := (d) if dur <= 0 { // The deadline has reached, cancel directly and cancel the mount from parent at the same time // Because it is timeout, the err when canceling is DeadlineExceeded (true, DeadlineExceeded) // Return to c and CancelFunc, and the mount has been cancelled. At this time, CancelFunc will not cancel the mount from parent. // Call CancelFunc again later will not produce any effect // If you cancel it voluntarily, err is Canceled return c, func() { (false, Canceled) } } // The deadline has not yet reached, define a timer, and after dur, it will be automatically cancelled () defer () if == nil { = (dur, func() { // Because the deadline is cancelled, err is DeadlineExceeded (true, DeadlineExceeded) }) } // Returns c and cancelFunc, the err that is actively canceled is Canceled return c, func() { (true, Canceled) } }
Next, let's look at the cancel method. The cancel method of timerCtx is called the cancel() method of embedded cancelCtx. By default, it is not removed from the parent node.
func (c *timerCtx) cancel(removeFromParent bool, err error) { (false, err) // Removing from the parent node if removeFromParent { removeChild(, c) } // Stop the timer and release resources // It is possible that before the deadline has arrived, CancelFunc has been manually triggered, and timer has been stopped at this time () if != nil { () = nil } () }
WithTimeout
WithTimeout is based on WithDeadline, deadline is based on the current time calculation.
func WithTimeout(parent Context, timeout ) (Context, CancelFunc) { return WithDeadline(parent, ().Add(timeout)) }
Summarize
In this article, we learned the structure and implementation logic of the context package through source code + examples, including the following content
Context interface: defines some interface methods and specifications
emptyCtx: The empty Context, Background() and TODO() methods are used by emptyCtx
valueCtx: used to save key-value pairs. It is a recursive query when querying. It can be used to save global id such as LogID.
cancelCtx: Cancel Context, used to cancel the signal transmission
timerCtx: cancelCtx canceled timer
The above is a detailed explanation of the usage of the context package for Go language learning. For more information about the Go language context package, please pay attention to my other related articles!