Learning and Communication: Go Language Technology WeChat Group
Business cooperation plus WeChat: LetsFeng
Learning and sharing: GoLand2022 Genuine Activation Code for the Family Bucket Universal Edition
Textbooks, documents to learn Go language, I strongly recommend this book
Start your Go language learning journey now! Life is too short, let's Go.
1. Introduction to context
Many times, we will encounter such a situation that the upper and lower goroutines need to be cancelled at the same time, which involves communication between goroutines. In Go, it is recommended that we share memory by way of communication, not by way of shared memory .
Therefore, channl needs to be used. However, in the above scenario, if you need to handle the business logic of channl yourself, there will be a lot of time-consuming and laborious repetitive work, so context appears.
Context is a method used for process communication in Go, and its bottom layer is implemented by means of channl and snyc.Mutex .
2. Basic introduction
The underlying design of context can be summarized as 1 interface, 4 implementations and 6 methods.
-
1 interface
-
Context specifies four basic methods of context
-
4 implementations
-
emptyCtx implements an empty context that can be used as the root node
-
cancelCtx implements a context with cancel function, which can be actively canceled
-
timerCtx implements a context that is periodically canceled by timer timer and deadline time deadline
-
valueCtx implements a context that can store data through key and val fields
-
6 methods
-
Background returns an emptyCtx as the root node
-
TODO returns an emptyCtx as unknown node
-
WithCancel returns a cancelCtx
-
WithDeadline returns a timerCtx
-
WithTimeout returns a timerCtx
-
WithValue returns a valueCtx
3. Source code analysis
3.1 Context interface
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
-
Deadline() : Returns a time.Time that indicates the time when the current Context should end, and ok indicates that there is an end time
-
Done(): Returns a read-only chan, if data can be read from the chan, it means ctx has been cancelled
-
Err(): Returns the reason why the Context was canceled
-
Value(key): Returns the value corresponding to the key, which is coroutine-safe
3.2 emptyCtx
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
}
emptyCtx implements the empty Context interface, and its main function is to return pre-initialized private variables background and todo for both Background and TODO methods, which will be reused in the same Go program:
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
There is no difference in implementation between Backgro und and TODO , only in usage semantics:
-
Background is the root node of the context;
-
TODO should only be used when not sure which context should be used;
3.3 cancelCtx
cancelCtx implements the canceler interface and the Context interface:
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
Its structure is as follows:
type cancelCtx struct {
// 直接嵌入了一个 Context,那么可以把 cancelCtx 看做是一个 Context
Context
mu sync.Mutex // protects following fields
done atomic.Value // of chan struct{}, created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
We can use the WithCancel method to create a cancelCtx:
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() { c.cancel(true, Canceled) }
}
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
In the above method, we pass in a parent Context (usually a background, as the root node), return the newly created context, and return a cancel method in the form of a closure.
newCancelCtx wraps the incoming context into a private structure context.cancelCtx.
propagateCancel will build the association between parent and child contexts to form a tree structure. When the parent context is cancelled, the child context will also be cancelled:
func propagateCancel(parent Context, child canceler) {
// 1.如果 parent ctx 是不可取消的 ctx,则直接返回 不进行关联
done := parent.Done()
if done == nil {
return // parent is never canceled
}
// 2.接着判断一下 父ctx 是否已经被取消
select {
case <-done:
// 2.1 如果 父ctx 已经被取消了,那就没必要关联了
// 然后这里也要顺便把子ctx给取消了,因为父ctx取消了 子ctx就应该被取消
// 这里是因为还没有关联上,所以需要手动触发取消
// parent is already canceled
child.cancel(false, parent.Err())
return
default:
}
// 3. 从父 ctx 中提取出 cancelCtx 并将子ctx加入到父ctx 的 children 里面
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
// double check 一下,确认父 ctx 是否被取消
if p.err != nil {
// 取消了就直接把当前这个子ctx给取消了
// parent has already been canceled
child.cancel(false, p.err)
} else {
// 否则就添加到 children 里面
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
// 如果没有找到可取消的父 context。新启动一个协程监控父节点或子节点取消信号
atomic.AddInt32(&goroutines, +1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
The above method may encounter the following situations:
-
When parent.Done() == nil, that is, when parent does not trigger the cancellation event, the current function will return directly;
-
When the child's inheritance chain contains a context that can be cancelled, it will determine whether the parent has triggered the cancellation signal;
-
If it has been cancelled, the child will be cancelled immediately;
-
If it is not cancelled, the child will be added to the parent's children list, waiting for the parent to release the cancellation signal;
-
When the parent context is a developer-defined type, implements the context.Context interface, and returns a non-empty pipe in the Done() method;
-
Run a new Goroutine to listen to the two Channels of parent.Done() and child.Done() at the same time;
-
Call child.cancel to cancel the child context when parent.Done() is closed;
The function of propagateCancel is to synchronize the cancellation and termination signals between the parent and the child, ensuring that when the parent is canceled, the child will also receive the corresponding signal, and there will be no state inconsistency.
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
done := parent.Done()
// 如果 done 为 nil 说明这个ctx是不可取消的
// 如果 done == closedchan 说明这个ctx不是标准的 cancelCtx,可能是自定义的
if done == closedchan || done == nil {
return nil, false
}
// 然后调用 value 方法从ctx中提取出 cancelCtx
p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
if !ok {
return nil, false
}
// 最后再判断一下cancelCtx 里存的 done 和 父ctx里的done是否一致
// 如果不一致说明parent不是一个 cancelCtx
pdone, _ := p.done.Load().(chan struct{})
if pdone != done {
return nil, false
}
return p, true
}
The done method of cancelCtx returns a chan struct{}:
func (c *cancelCtx) Done() <-chan struct{} {
d := c.done.Load()
if d != nil {
return d.(chan struct{})
}
c.mu.Lock()
defer c.mu.Unlock()
d = c.done.Load()
if d == nil {
d = make(chan struct{})
c.done.Store(d)
}
return d.(chan struct{})
}
var closedchan = make(chan struct{})
parentCancelCtx is actually to judge whether there is a cancelCtx in the parent context, and return if there is, so that the child context can be "attached" to the parent context, if not, return false, do not attach, and open a new goroutine to monitor.
3.4 timerCtx
TimerCtx not only inherits related variables and methods by embedding cancelCtx, but also realizes the function of timing cancellation by holding the timer timer and deadline time deadline:
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(false, err)
if removeFromParent {
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
3.5 valueCtx
valueCtx has two more fields, key and val, to store data:
type valueCtx struct {
Context
key, val interface{}
}
The process of value search is actually a recursive search process:
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
If the key is consistent with the value stored in the current ctx, it will return directly, if not, go to the parent to find it. Finally, the root node (usually emptyCtx) is found, and a nil is returned directly. Therefore, when using the Value method, it is necessary to judge whether the result is nil, which is similar to a linked list, and the efficiency is very low. It is not recommended to pass parameters.
4. Recommendations for use
In the official blog, there are several suggestions for using context:
-
Don't stuff Context into structs. The Context type is directly used as the first parameter of the function, and it is generally named ctx.
-
Don't pass a nil context to the function. If you really don't know what to pass, the standard library has prepared a context for you: todo.
-
Don't stuff the types that should be used as function parameters into the context, the context should store some common data. For example: login session, cookie, etc.
-
The same context may be passed to multiple goroutines, don't worry, context is concurrency safe.
Reference link: https://juejin.cn/post/7106157600399425543
For more technical articles or video tutorials related to the Go language, please follow this official account to obtain and view, thank you for your support and trust!