Original link: Detailed explanation of the context package that Xiaobai can understand: from entry to proficiency
Preface
Hello, everyone, my name is asong
. Today I want to share with you the context
package. After a year of precipitation 1.17.1
, but the difference this time is that I plan to start from the beginning, because most beginner readers want to know first How to use it, and then you will care about how the source code is implemented.
I believe you will see such code in your daily work and development:
func a1(ctx context ...){
b1(ctx)
}
func b1(ctx context ...){
c1(ctx)
}
func c1(ctx context ...)
context
is used as the first parameter (official recommendation), and it is continuously passed on. Basically, a project code is full of context
, but do you really know what it does and how it works? I remember that when I first contacted context
, my colleagues said that this is used for concurrency control. You can set a timeout, and the timeout will cancel the execution and return quickly. I simply think that as long as the function carries the context
parameter to You can cancel the timeout and return quickly after the next pass. I believe that most beginners have the same idea with me. In fact, this is a wrong idea. The cancellation mechanism is also a notification mechanism. Pure transmission will not work. For example, if you write code like this:
func main() {
ctx,cancel := context.WithTimeout(context.Background(),10 * time.Second)
defer cancel()
go Monitor(ctx)
time.Sleep(20 * time.Second)
}
func Monitor(ctx context.Context) {
for {
fmt.Print("monitor")
}
}
context
passed through, it will not have any effect if it is not used. Therefore context
. This article will start with the use and gradually analyze the context
Go
language. Let’s start now! ! !
The origin and function of the context
Looking at the official blog, we can know that the context
package was introduced into the standard library in the go1.7
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c0d332914b0c44ae8706589eaef6ebaa~tplv-k3u1fbpfcp-zoom-1.image" style="zoom:50%;" />
context
can be used to transfer context information between goroutine
context
can be passed to functions running in different goroutine
. The context goroutine
at the same time. The context
package defines the context type and can be created background
, TODO
For a context, to propagate context
between function call chains, you can also use WithDeadline
, WithTimeout
, WithCancel
or WithValue
replace it. It sounds a bit context
is different from goroutine
Inter-synchronization requests specific data, cancellation signals, and deadlines for processing the request.
At present, some of our commonly used libraries support context
. For example, libraries gin
database/sql
context
. This makes it more convenient for us to control concurrency. Just create a context
context at the server entrance and continue to pass it through.
context
Created context
context
package mainly provides two ways to create context
:
context.Backgroud()
context.TODO()
These two functions are actually aliases for each other, there is no difference, the official definition is:
context.Background
is the default value of the context, and all other contexts should be derived from it.context.TODO
should only be used when not sure which context should be used;
So in most cases, we use context.Background
as the starting context to pass down.
The above two methods are to create the root context
, which does not have any functions. The specific practice still depends on the With
series of functions context
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
These four functions must be Context
. Through these functions, a Context tree is created. Each node of the tree can have any number of child nodes, and there can be any number of node levels. Draw a picture to show:
Based on a parent Context
can be derived at will. In fact, this is a Context
tree. Each node of the tree can have any number of child nodes, and there can be any number of node levels, and each child node depends on its parent node. For example, We can derive four children context
Context.Background
: ctx1.0-cancel
, ctx2.0-deadline
, ctx3.0-timeout
, ctx4.0-withvalue
. These four children context
can also be used as the parent context
other three nodes of 0618500b1c8505 will continue to be derived, even if the ctx1.0-cancel
That's all for creating the context
method and the context
derivative methods. Let's take a look at how they are used one by one.
WithValue
carries data
In our daily business development, we hope to have a trace_id
that can connect all the logs in series. This requires us to get this trace_id
when we print the logs. In python
we can use gevent.local
to transmit, and in java
we can use ThreadLocal
to transmit , In the Go
language, we can use Context
to pass, by using WithValue
to create a trace_id
context
, and then continue to pass on, and print the log output, just look at the usage example:
const (
KEY = "trace_id"
)
func NewRequestID() string {
return strings.Replace(uuid.New().String(), "-", "", -1)
}
func NewContextWithTraceID() context.Context {
ctx := context.WithValue(context.Background(), KEY,NewRequestID())
return ctx
}
func PrintLog(ctx context.Context, message string) {
fmt.Printf("%s|info|trace_id=%s|%s",time.Now().Format("2006-01-02 15:04:05") , GetContextValue(ctx, KEY), message)
}
func GetContextValue(ctx context.Context,k string) string{
v, ok := ctx.Value(k).(string)
if !ok{
return ""
}
return v
}
func ProcessEnter(ctx context.Context) {
PrintLog(ctx, "Golang梦工厂")
}
func main() {
ProcessEnter(NewContextWithTraceID())
}
Output result:
2021-10-31 15:13:25|info|trace_id=7572e295351e478e91b1ba0fc37886c0|Golang梦工厂
Process finished with the exit code 0
We create a trace_id
with ctx
based on context.Background
, and then pass it through the context
tree. Any context
derived from it will get this value. When we finally print the log, we can take the value from ctx
and output it to the log. At present, some RPC
frameworks support Context
, so the downward transmission of trace_id
withVaule
are four things to pay attention to when using 0618500b1c8788:
- Not recommended
context
value passed key parameters, declare the key parameters to be displayed out and should not be treated implicitly,context
is best to carry the signature,trace_id
such values. - Because
value
is alsokey
andvalue
, in order to avoidcontext
context
multiple packages at the same time for 0618500b1c885f,key
recommended to use the built-in type. - In the above example, we get
trace_id
directly from the currentctx
. In fact, we can also get thecontext
in thevalue
. When obtaining the key-value pair, we firstcontext
. If we don’t find it, we will look it upcontext
The value corresponding to the key untilcontext
is returnednil
or the corresponding value is found. context
the data passed bykey
,value
are all ofinterface
. This type cannot be determined during compile time, so it is not very safe, so don't forget to ensure the robustness of the program when type assertion.
Timeout control
Usually robust programs need to set a timeout to avoid resource consumption due to long-time server response. Therefore, some web
frameworks or rpc
frameworks will use withTimeout
or withDeadline
for timeout control. When a request reaches the timeout time we set, it will It will be cancelled in time and will not be executed further. withTimeout
and withDeadline
have the same function, that is, the time parameters passed are different. They will automatically cancel Context
through the passed time. It should be noted that they will all return a cancelFunc
method. By calling this method, you can cancel in advance. However, it is recommended to call cancelFunc
after the automatic cancellation to stop the timing to reduce unnecessary waste of resources.
withTimeout
, WithDeadline
except that WithTimeout
duration time as input parameters rather than objects, which use these two methods are the same, see business scenarios and personal habits, because of the nature withTimout
inside also called WithDeadline
.
Now we give an example to try out the timeout control. Now we will simulate a request and write two examples:
- The next execution is terminated when the timeout period is reached
func main() {
HttpHandler()
}
func NewContextWithTimeout() (context.Context,context.CancelFunc) {
return context.WithTimeout(context.Background(), 3 * time.Second)
}
func HttpHandler() {
ctx, cancel := NewContextWithTimeout()
defer cancel()
deal(ctx)
}
func deal(ctx context.Context) {
for i:=0; i< 10; i++ {
time.Sleep(1*time.Second)
select {
case <- ctx.Done():
fmt.Println(ctx.Err())
return
default:
fmt.Printf("deal time is %d\n", i)
}
}
}
Output result:
deal time is 0
deal time is 1
context deadline exceeded
- The timeout period is not reached to terminate the next execution
func main() {
HttpHandler1()
}
func NewContextWithTimeout1() (context.Context,context.CancelFunc) {
return context.WithTimeout(context.Background(), 3 * time.Second)
}
func HttpHandler1() {
ctx, cancel := NewContextWithTimeout1()
defer cancel()
deal1(ctx, cancel)
}
func deal1(ctx context.Context, cancel context.CancelFunc) {
for i:=0; i< 10; i++ {
time.Sleep(1*time.Second)
select {
case <- ctx.Done():
fmt.Println(ctx.Err())
return
default:
fmt.Printf("deal time is %d\n", i)
cancel()
}
}
}
Output result:
deal time is 0
context canceled
It is relatively easy to use, it can be automatically cancelled over time, and can also be manually controlled to cancel. context
in the call link that is transparently transmitted from the request entry. If we want to open a goroutine in it to handle other things and it will not end with the request If it is cancelled later, then the transmitted context
should be context.Background
or context.TODO
, and the veto will be inconsistent with expectations. You can read my previous article on the pit: A bug caused by improper context usage .
withCancel
cancel control
In daily business development, we often open multiple gouroutine
do some things in order to complete a complex requirement, which leads us to open multiple goroutine
in one request, but we cannot control them, then we can use withCancel
to derive A context
passed to a different goroutine
. When I want to goroutine
, I can call cancel
to cancel it.
Let's look at an example:
func main() {
ctx,cancel := context.WithCancel(context.Background())
go Speak(ctx)
time.Sleep(10*time.Second)
cancel()
time.Sleep(1*time.Second)
}
func Speak(ctx context.Context) {
for range time.Tick(time.Second){
select {
case <- ctx.Done():
fmt.Println("我要闭嘴了")
return
default:
fmt.Println("balabalabalabala")
}
}
}
operation result:
balabalabalabala
....省略
balabalabalabala
我要闭嘴了
We use withCancel
create a Background
, and then start a speech program to speak every 1s, main
function executes cancel
after 10s, then speak
will exit when it detects the cancellation signal.
Custom Context
Because Context
is essentially an interface, so we can achieve Context
achieve custom Context
purposes, typically implemented Web
frame or RPC
frame is frequently used to form such gin
frame Context
that they have a layer of packaging, and the specific code The implementation is posted here. If you are interested, you can see how gin.Context
is implemented.
Appreciation of source code
Context is actually an interface, which defines four methods:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Deadlne
method: whenContext
automatically cancelled or it is cancelled when the cancellation time is reached, returnDone
method: whenContext
is cancelled ordeadline
return a closedchannel
Err
method: whenContext
is cancelled or closed, return the reason for the cancellation ofcontext
Value
method: get the value corresponding to thekey
This interface is mainly inherited and implemented by three classes, namely emptyCtx
, ValueCtx
, cancelCtx
, using the wording of an anonymous interface, so that any type that implements the interface can be rewritten.
Below we will analyze layer by layer from creation to use.
Create root Context
The object created when we call context.Background
and context.TODO
empty
:
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
Background
and TODO
still exactly the same, the official said: background
is usually used by the main function, initialization and testing, and as the top-level context of the incoming request; TODO
is when it is not clear which Context to use or is not yet available, the code should use the context. TODO is being replaced in the follow-up. In the final analysis, the semantics are different.
emptyCtx
category
emptyCtx
mainly used when we create the root Context
. The implementation method is also an empty structure. The actual source code looks like this:
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
}
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
WithValue
withValue
internal part of 0618500b1ca83e is mainly to call the valueCtx
class:
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 !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
valueCtx
category
valueCtx
purpose of Context
is to carry key-value pairs for 0618500b1ca8ad, because it uses the inheritance implementation of anonymous interfaces, it will inherit the parent Context
, which is equivalent to embedding in Context
type valueCtx struct {
Context
key, val interface{}
}
Implemented the String
method to output Context
and the key-value pair information carried:
func (c *valueCtx) String() string {
return contextName(c.Context) + ".WithValue(type " +
reflectlite.TypeOf(c.key).String() +
", val " + stringify(c.val) + ")"
}
Implement the Value
method to store key-value pairs:
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
Look at the picture to understand:
So we call Context
in Value
will call up the layers until the final method when the root of the middle and if found key
will return, otherwise will will find the ultimate emptyCtx
return nil
.
WithCancel
Let's take a look at the source code of the entry function of WithCancel
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) }
}
The execution steps of this function are as follows:
- Create a
cancelCtx
object as the childcontext
- Then call
propagateCancel
build the relationship between the father and soncontext
, so that when the parentcontext
is cancelled, the childcontext
will also be cancelled. - Return
context
object and subtree cancellation function
Let's analyze the class cancelCtx
cancelCtx
Class 6
cancelCtx
inherits Context
and also implements the interface canceler
:
type cancelCtx struct {
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
}
Short explanation:
mu
: It is a mutual exclusion lock to ensure concurrency safety, socontext
is concurrency safetydone
: used to make the cancellation notification signal ofcontext
chan struct{}
type, nowatomic.Value
is used for lock optimizationchildren
:key
is the interface typecanceler
, the purpose is to storecanceler
interface, when the root node is cancelled, traverse the child nodes to send a cancellation signalerror
: Store cancellation informationcontext
Done
method is implemented here, and the return is a read-only channel
, the purpose is that we can wait for the notification signal channel
The specific code will not be posted. Let's go back and see propagateCancel
is used to construct the Context
between father and son 0618500b1cac45.
propagateCancel
method
The code is a bit long and the explanation is a bit cumbersome. I added comments to the code and it seems more intuitive:
func propagateCancel(parent Context, child canceler) {
// 如果返回nil,说明当前父`context`从来不会被取消,是一个空节点,直接返回即可。
done := parent.Done()
if done == nil {
return // parent is never canceled
}
// 提前判断一个父context是否被取消,如果取消了也不需要构建关联了,
// 把当前子节点取消掉并返回
select {
case <-done:
// parent is already canceled
child.cancel(false, parent.Err())
return
default:
}
// 这里目的就是找到可以“挂”、“取消”的context
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
// 找到了可以“挂”、“取消”的context,但是已经被取消了,那么这个子节点也不需要
// 继续挂靠了,取消即可
if p.err != nil {
child.cancel(false, p.err)
} else {
// 将当前节点挂到父节点的childrn map中,外面调用cancel时可以层层取消
if p.children == nil {
// 这里因为childer节点也会变成父节点,所以需要初始化map结构
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
// 没有找到可“挂”,“取消”的父节点挂载,那么就开一个goroutine
atomic.AddInt32(&goroutines, +1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
What really puzzles this code is this if, else branch. Don't look at the code, just say why. Because we can customize context
, context
into a structure, it will not find the cancelable parent node, and we can only restart a coroutine to monitor.
If you are confused about this piece, I recommend reading Rao article: 1618500b1cad26 [Depth Deciphering the Go Language Context ] ( https://www.cnblogs.com/qcrao-2018/p/11007503.html), it will definitely be able to arrange it for you Worry about confusion.
cancel
method
Finally, let's take a look at how the returned cancel
method is implemented. This method will close the Channel in the context and cancel the signal synchronously to all sub-contexts:
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
// 取消时传入的error信息不能为nil, context定义了默认error:var Canceled = errors.New("context canceled")
if err == nil {
panic("context: internal error: missing cancel error")
}
// 已经有错误信息了,说明当前节点已经被取消过了
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
// 用来关闭channel,通知其他协程
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
} else {
close(d)
}
// 当前节点向下取消,遍历它的所有子节点,然后取消
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
// 节点置空
c.children = nil
c.mu.Unlock()
// 把当前节点从父节点中移除,只有在外部父节点调用时才会传true
// 其他都是传false,内部调用都会因为c.children = nil被剔除出去
if removeFromParent {
removeChild(c.Context, c)
}
}
At this point, the WithCancel
method is analyzed. Through the source code, we can know that the cancel
method can be called repeatedly and is idempotent.
withDeadline
and WithTimeout
WithTimeout
look at the 0618500b1cae15 method. Inside it is the WithDeadline
method called:
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
So let’s focus on withDeadline
is implemented:
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
// 不能为空`context`创建衍生context
if parent == nil {
panic("cannot create context from nil parent")
}
// 当父context的结束时间早于要设置的时间,则不需要再去单独处理子节点的定时器了
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
// 创建一个timerCtx对象
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
// 将当前节点挂到父节点上
propagateCancel(parent, c)
// 获取过期时间
dur := time.Until(d)
// 当前时间已经过期了则直接取消
if dur <= 0 {
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(false, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
// 如果没被取消,则直接添加一个定时器,定时去取消
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
withDeadline
compared to withCancel
method also more a timer to regularly call cancel
method, this cancel
method in timerCtx
rewritten class, we first look at timerCtx
class, he is based cancelCtx
, and two more fields :
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
timerCtx
implemented cancel
method, also called internal cancelCtx
of cancel
method to cancel:
func (c *timerCtx) cancel(removeFromParent bool, err error) {
// 调用cancelCtx的cancel方法取消掉子节点context
c.cancelCtx.cancel(false, err)
// 从父context移除放到了这里来做
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
// 停掉定时器,释放资源
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
Finally, we have finished reading the source code part, what do you think now?
context
and disadvantages of 0618500b1caf67
context
package is designed for concurrency control. This package has advantages and disadvantages. I have summarized several advantages and disadvantages. Welcome to add in the comment area.
shortcoming
- Affects the beauty of the code. Now basically all the
web
andRPC
frameworks implementcontext
, which leads to a parameter of each function in our code iscontext
, even if you don’t need to take this parameter to pass it through, personally feel a little bit ugly. context
can carry values, but there are no restrictions. There are no restrictions on the type and size, that is, there are no restrictions. This can easily lead to abuse, and the robustness of the program is difficult to guarantee; there is another problem thatcontext
is not as good as explicitly passing values Comfortable, the readability has deteriorated.- You can customize
context
, so the risk is uncontrollable and will lead to abuse. context
The error return of cancellation and automatic cancellation is not friendly enough, you cannot customize the error, and it is difficult to troubleshoot when there are problems that are difficult to troubleshoot.- Creating a derivative node is actually creating a linked list node, and its time complexity is O(n). If there are more nodes, the efficiency of dropping branches will become lower.
advantage
- Using
context
can better control the concurrency and better manage the abuse ofgoroutine
context
, so we can transmit any data, which can be said to be a double-edged sword- Online said
context
package to solve thegoroutine
ofcancelation
question, do you think?
Reference article
https://pkg.go.dev/context@go1.7beta1#Background
https://studygolang.com/articles/21531
https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-context/
https://www.cnblogs.com/qcrao-2018/p/11007503.html
https://segmentfault.com/a/1190000039294140
https://www.flysnow.org/2017/05/12/go-in-action-go-context.html
Summarize
context
is a bit ugly in use, it can solve many problems. In daily business development, it is inseparable from context
, but do not use the wrong context
. The cancellation also uses the channel
notification, so there are still some requirements in the code. There is a monitoring code to monitor the cancellation signal, which is often overlooked by the majority of beginners.
The example in the article has been uploaded github
: https://github.com/asong2020/Golang_Dream/tree/master/code_demo/context_example
, this is the end of this article. I am asong
. See you in the next issue.
**Welcome to follow the public account: [Golang DreamWorks]
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。