Understand the Go standard library context package through examples

Permalink to this article – https://ift.tt/YMmGSxl

Since the context package was added to the Go standard library in Go 1.7 , it has become one of the more difficult and misused packages in the Go standard library. There is currently no article on my blog that systematically introduces the context package. Many readers from the Go column or “The Road to Go Language Improvement” hope that I can write an article about the context package. I will try it today^ _^.

1. The context is included in the standard library process

In 2014, Sameer Ajmani, a core member of the Go team, published an article “Go Concurrency Patterns: Context” on the Go official blog, introducing a package called context designed and implemented by Google and the results of this package after being practiced within Google. some application modes. Subsequently, the package was open sourced and maintained under golang.org/x/net/context. Two years later, in 2016, the golang.org/x/net/context package was officially moved into the Go standard library, which is the birth process of the current Go standard library context package.

Historical experience tells us that whenever Google thinks it is a good thing, it basically ends up in the Go language or the standard library . The context package is one of them, and the type alias syntax added in the subsequent Go 1.9 version also confirms this. It can be predicted that the arena package , which will be added as an experimental feature in Go 1.20, is only a matter of time before it finally officially joins Go ^_^!

2. What problem does the context package solve?

Defining the problem correctly is more important than solving it . In Sameer Ajmani’s article, he clearly explained the problem to be solved by introducing the context package at the beginning:

In a Go server, each incoming request is processed in its own goroutine. Request handlers often start additional goroutines to access backend services such as database and RPC services. A group of goroutines handling a request usually needs access to specific values ​​associated with the request, such as the end user’s identity, authorization token, and request deadline. When a request is cancelled or processing times out, all goroutines working on that request should exit quickly so that the system can reclaim any resources they were using.

From this description, I got at least two points:

  • pass by value

The back-end service program has such a requirement that when calling other functions in the function (Handler Function) that processes a request, it passes the value information related to the request (request-specific) and other than the content of the request (hereinafter referred to as the context). value information in) , as shown in the following figure:

We see that this kind of function call and value transfer can occur between functions of the same goroutine (for example, the Handler function in the figure above calls the middleware function), between multiple goroutines in the same process (for example, the called function creates a new goroutine), or even between goroutines of different processes (such as rpc calls).

  • control

When a function call occurs due to the processing of an external request (request) under the same goroutine, if the called function (callee) does not start a new goroutine or perform cross-process processing (such as rpc calls), it is more often between functions. Pass-by-value, that is, pass the value information in the context.

But when the called function (callee) starts a new goroutine or does cross-process processing, this will usually be an asynchronous call . Why start a new goroutine to make asynchronous calls? More for control . If it is a synchronous call, once the callee has a delay or failure, the call is likely to be blocked for a long time. The caller can neither eliminate this impact nor recycle the various resources applied for processing the request in time, let alone. Guaranteed SLAs between service interfaces.

Note: The caller and the callee can be a synchronous call or an asynchronous call, and the callee usually starts a new goroutine to implement an “asynchronous call”.

So how to control asynchronous calls? This time, what we pass between the caller and the callee is no longer a value information, but a “tacit agreement”, that is, a control mechanism, as shown in the following figure:

When the callee completes the task within the limited time of the caller, the call is successful, and the callee releases all resources; when the callee cannot complete the task within the limited time or the callee receives a notification that the caller cancels the call, it can also End the call and release resources.

Next, let’s take a look at how the Go standard library context package solves the above two problems.

3. The composition of the context package

Go has unified the solutions for the above two problems of “pass by value and control” into an interface type called Context under the context package:

 // $GOROOT/src/context/context.go type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key any) any }

Note: There is no unified standard for “context”, and many third-party packages also have their own definitions of Context, but after Go 1.7, they have gradually switched to using the context.Context of the Go standard library.

If you understand the problems to be solved by the previous context package, you can roughly divide the methods in the Context interface type into two categories. The first category is the Value method, which is used to solve the problem of “passing by value”; the other three methods (Deadline, Done, and Err) fall into the second category and are used to solve the problem of “passing control”.

If you just define an interface type like Context and unify the abstraction of Context, then things will not be completely solved (but it is better than the log package ). Convenience functions and implementations of several built-in Context interfaces. Let’s take a look at them one by one.

1) WithValue function

First let’s look at the WithValue function for passing values .

 // $GOROOT/src/context/context.go func WithValue(parent Context, key, val any) Context

The WithValue function creates a new Context based on the parent Context. This new Context not only saves a copy of the parent Context, but also saves the key-val pair accepted by the WithValue function. WithValue actually returns an instance of type named *valueCtx. *valueCtx implements the Context interface, which consists of three fields:

 // $GOROOT/src/context/context.go type valueCtx struct { Context key, val any }

Combined with the implementation logic of WithValue, the Context in valueCtx is assigned as the parent Context, and the key and val respectively save the key and val passed in by WithValue.

After the new Context is successfully created, the processing function will subsequently transmit the value information in the context based on the new Context. Let’s look at an example:

 // github.com/bigwhite/experiments/tree/master/context-examples/with_value/main.go package main import ( "context" "fmt" ) func f3(ctx context.Context, req any) { fmt.Println(ctx.Value("key0")) fmt.Println(ctx.Value("key1")) fmt.Println(ctx.Value("key2")) } func f2(ctx context.Context, req any) { ctx2 := context.WithValue(ctx, "key2", "value2") f3(ctx2, req) } func f1(ctx context.Context, req any) { ctx1 := context.WithValue(ctx, "key1", "value1") f2(ctx1, req) } func handle(ctx context.Context, req any) { ctx0 := context.WithValue(ctx, "key0", "value0") f1(ctx0, req) } func main() { rootCtx := context.Background() handle(rootCtx, "hello") }

In the above code, handle is the entry function responsible for processing the “request”. It accepts a root Context created by the main function and the request content itself (“hello”). After that, the handle function is based on the incoming ctx and passes the WithValue function. Created a new Context containing its own additional key0-value0 pair, this new Context will be passed to f1 as a context when calling the f1 function; and so on, f1, f2 are based on the incoming ctx through the WithValue function to create a new context containing itself The new Context of additional value information, at the end of the function call chain, f3 tries to retrieve various value information in the context from the incoming ctx through the Value method of Context. We use a schematic diagram to show this process:

Let’s run the above code to see the result:

 $go run main.go value0 value1 value2

We can see that f3 not only takes out the key2-value2 attached to f2 from the context, but also takes out the value information attached to functions such as handle and f1. This is due to the implementation of the *valueCtx type that satisfies the Context interface “follow the trails”:

 // $GOROOT/src/context/context.go func (c *valueCtx) Value(key any) any { if c.key == key { return c.val } return value(c.Context, key) } func value(c Context, key any) any { for { switch ctx := c.(type) { case *valueCtx: if key == ctx.key { return ctx.val } c = ctx.Context case *cancelCtx: if key == &cancelCtxKey { return c } c = ctx.Context case *timerCtx: if key == &cancelCtxKey { return &ctx.cancelCtx } c = ctx.Context case *emptyCtx: return nil default: return c.Value(key) } } }

We see that in the *valueCtx case, if the key is different from the current ctx key, it will continue to search along the parent Ctx path until it is found.

We see: WithValue is easy to use and easy to understand. However, since each valueCtx can only save a pair of key-vals , even if multiple value information is added to a function, its usage pattern must be as follows:

 ctx1 := WithValue(parentCtx, key1, val1) ctx2 := WithValue(ctx1, key2, val2) ctx3 := WithValue(ctx2, key3, val3) nextCall(ctx3, req)

rather than

 ctx1 := WithValue(parentCtx, key1, val1) ctx1 = WithValue(parentCtx, key2, val2) ctx1 = WithValue(parentCtx, key3, val3) nextCall(ctx1, req)

Otherwise, only the last key3-val3 information will be saved in ctx1, and key1 and key2 will be overwritten.

This design of valueCtx also results in the inefficient key lookup of the Value method, which is an O(n) lookup. In some performance-sensitive web frameworks, valueCtx and WithValue may be difficult to use.

In the above example, we talked about the root Context, let’s briefly talk about the construction of the root Context.

2) Root Context Construction

The root Context, also known as the top-level Context, is the top-level Context, which is usually created in the main function, initialization function, and request processing entry (a Handle function). Go provides two root Context construction methods Background and TODO:

 // $GOROOT/src/context/context.go var ( background = new(emptyCtx) todo = new(emptyCtx) ) func Background() Context { return background } func TODO() Context { return todo }

We see that although the standard library provides two methods for creating root Context, they are essentially the same, and the bottom layer returns an instance of emptyCtx type that has the same life cycle as the program. Some friends may ask: Is there a problem with all Go code sharing a root Context?

The answer is no! Because the root Context does not do any “real things”, just like the “King of the Commonwealth”, it only has a nominal symbolic meaning. It neither stores context value information nor carries context control information. will not be changed. It is only a pointer to the parent Context of the second-level context. The Context that really has a “functional” role is a second-level Context similar to the Prime Minister or Prime Minister:

Usually we use the Background() function to construct the root Context, and according to the comments of the TODO function of the context package, TODO is only used temporarily when it is not clear which Context should be used.

3) WithCancel function

The WithCancel function provides the first control mechanism for the context: cancel, which is also the basis for the control mechanism of the entire context package. Let’s first intuitively feel the role of WithCancel. Here is an example from the Go context package documentation :

 package main import ( "context" "fmt" ) func main() { gen := func(ctx context.Context) <-chan int { dst := make(chan int) n := 1 go func() { for { select { case <-ctx.Done(): return // returning not to leak the goroutine case dst <- n: n++ } } }() return dst } ctx, cancel := context.WithCancel(context.Background()) defer cancel() // cancel when we are finished consuming integers for n := range gen(ctx) { fmt.Println(n) if n == 5 { break } } }

In this example, the main function creates a Context instance with a cancelable property via WithCancel, and then passes that instance when calling the gen function. In addition to returning a Context instance with a cancelable property, the WithCancel function also returns a cancelFunc, which is the “button” held in the caller’s hand. Once the “button” is pressed, the caller issues “Cancel” Signal, the goroutine started in the asynchronous call should drop the work at hand and exit honestly.

Just like the above example, after the main function passes the cancel Context to gen, the gen function starts a new goroutine to generate a set of arrays, and the main function reads the numbers in these arrays from the channel returned by gen. After the main function reads the fifth number, it presses the “button”, that is, the cancel Function is called. At this time, the goroutine that generates the array will listen to the event of the Done channel, and then complete the exit of the goroutine.

This is the kind of “tacit agreement” that should exist between the caller and the callee (and the new goroutine created by the caller) as mentioned earlier, and this “tacit agreement” requires both to be based on the context according to a certain ” In this example, the caller calls the cancel Function in a timely manner, and the goroutine started by gen listens to the Done channel that can cancel the Context instance .

And usually, after we create a cancel Context, we immediately register the cancel Function to the deferred function stack through defer to prevent resource leakage caused by not calling the cancel Function! In this example, if the cancel Function is not called, the goroutine created by the gen function will always run, although the numbers it generates will no longer be consumed by other goroutines.

Compared with the WithValue function, the implementation of WithCancel is slightly more complicated:

 // $GOROOT/src/context/context.go 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} }

The complexity is in the call of propagateCancel:

 // propagateCancel arranges for child to be canceled when parent is. func propagateCancel(parent Context, child canceler) { done := parent.Done() if done == nil { return // parent is never canceled } select { case <-done: // parent is already canceled child.cancel(false, parent.Err()) return default: } if p, ok := parentCancelCtx(parent); ok { p.mu.Lock() if p.err != nil { // parent has already been canceled child.cancel(false, p.err) } else { if p.children == nil { p.children = make(map[canceler]struct{}) } p.children[child] = struct{}{} } p.mu.Unlock() } else { atomic.AddInt32(&goroutines, +1) go func() { select { case <-parent.Done(): child.cancel(false, parent.Err()) case <-child.Done(): } }() } }

propagateCancel searches up the parent path through parentCancelCtx. The reason for this is that the Value method has the characteristics of searching along the parent path:

 func parentCancelCtx(parent Context) (*cancelCtx, bool) { done := parent.Done() if done == closedchan || done == nil { return nil, false } p, ok := parent.Value(&cancelCtxKey).(*cancelCtx) // 沿着parent路径查找第一个cancelCtx if !ok { return nil, false } pdone, _ := p.done.Load().(chan struct{}) if pdone != done { return nil, false } return p, true }

If you find a cancelCtx, add yourself to the child map of the cancelCtx:

 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 }

Note: The interface type value supports comparison. If the dynamic type of the two interface type values ​​is the same and the value of the dynamic type is the same, then the two interface type values ​​are the same. This is also the reason why the children map uses the canceler interface as the key.

In this way, when the cancel Function of its parent cancelCtx is called, the cancel function will call the cancel method of cancelCtx, the cancel method will traverse all children cancelCtx, and then call the cancel method of the child to achieve the purpose of associated cancellation, and the parent cancelCtx will be associated with all children. cancelCtx cancels the relationship!

 func (c *cancelCtx) cancel(removeFromParent bool, err error) { 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 d, _ := c.done.Load().(chan struct{}) if d == nil { c.done.Store(closedchan) } else { close(d) } for child := range c.children { // 遍历children,调用cancel方法// NOTE: acquiring the child's lock while holding parent's lock. child.cancel(false, err) } c.children = nil // 解除与children的关系c.mu.Unlock() if removeFromParent { removeChild(c.Context, c) } }

Let’s demonstrate with an example:

 // github.com/bigwhite/experiments/tree/master/context-examples/with_cancel/cancelctx_map.go package main import ( "context" "fmt" "time" ) // 直接使用parent cancelCtx func f1(ctx context.Context) { go func() { select { case <-ctx.Done(): fmt.Println("goroutine created by f1 exit") } }() } // 基于parent cancelCtx创建新的cancelCtx func f2(ctx context.Context) { ctx1, _ := context.WithCancel(ctx) go func() { select { case <-ctx1.Done(): fmt.Println("goroutine created by f2 exit") } }() } // 使用基于parent cancelCtx创建的valueCtx func f3(ctx context.Context) { ctx1 := context.WithValue(ctx, "key3", "value3") go func() { select { case <-ctx1.Done(): fmt.Println("goroutine created by f3 exit") } }() } // 基于parent cancelCtx创建的valueCtx之上创建cancelCtx func f4(ctx context.Context) { ctx1 := context.WithValue(ctx, "key4", "value4") ctx2, _ := context.WithCancel(ctx1) go func() { select { case <-ctx2.Done(): fmt.Println("goroutine created by f4 exit") } }() } func main() { valueCtx := context.WithValue(context.Background(), "key0", "value0") cancelCtx, cf := context.WithCancel(valueCtx) f1(cancelCtx) f2(cancelCtx) f3(cancelCtx) f4(cancelCtx) time.Sleep(3 * time.Second) fmt.Println("cancel all by main") cf() time.Sleep(10 * time.Second) // wait for log output }

The above example demonstrates four cases:

  • f1: use parent cancelCtx directly
  • f2: Create a new cancelCtx based on parent cancelCtx
  • f3: Use valueCtx created based on parent cancelCtx
  • f4: Use cancelCtx created on top of valueCtx created based on parent cancelCtx

Running this example, we get:

 cancel all by main goroutine created by f1 exit goroutine created by f2 exit goroutine created by f3 exit goroutine created by f4 exit

We can see that whether you use parent cancelCtx directly or use other Ctx created based on parent cancelCtx, when the cancel Function of parent cancelCtx is called, all goroutines listening to the corresponding child Done channel can be notified correctly and exit.

Of course, this “cancellation notification” can only be notified to the following children by the parent, but not vice versa. The parent cancelCtx will not be canceled because the cancel function of the child Context is called. In addition, if the cancel Function of a child cancelCtx is called, the child will be unbound from its parent cancelCtx.

In the implementation of the propagateCancel function posted earlier, we also saw another branch, that is, when the ok returned by the parentCancelCtx function is false, the propagateCancel function will start a new goroutine to monitor the parent Done channel and its own Done channel. Under what circumstances will it go to this execution branch? This doesn’t seem to be the case much! Let’s look at a case of custom cancelCtx:

 package main import ( "context" "fmt" "runtime" "time" ) func f1(ctx context.Context) { ctx1, _ := context.WithCancel(ctx) go func() { select { case <-ctx1.Done(): fmt.Println("goroutine created by f1 exit") } }() } type myCancelCtx struct { context.Context done chan struct{} err error } func (ctx *myCancelCtx) Done() <-chan struct{} { return ctx.done } func (ctx *myCancelCtx) Err() error { return ctx.err } func WithMyCancelCtx(parent context.Context) (context.Context, context.CancelFunc) { var myCtx = &myCancelCtx{ Context: parent, done: make(chan struct{}), } return myCtx, func() { myCtx.done <- struct{}{} myCtx.err = context.Canceled } } func main() { valueCtx := context.WithValue(context.Background(), "key0", "value0") fmt.Println("before f1:", runtime.NumGoroutine()) myCtx, mycf := WithMyCancelCtx(valueCtx) f1(myCtx) fmt.Println("after f1:", runtime.NumGoroutine()) time.Sleep(3 * time.Second) mycf() time.Sleep(10 * time.Second) // wait for log output }

In this example, we “partially escaped” the context cancelCtx system and customized a myCancelCtx that implements the Context interface. In this case, when the f1 function builds its own child CancelCtx based on myCancelCtx, it cannot be found upward because *cancelCtx type, so it WithCancel starts a goroutine to monitor both its own Done channel and the Done channel of its parent Ctx (ie myCancelCtx).

When the cancel Function of myCancelCtx is called in the main function (mycf()), the newly created goroutine will call the cancel function of the child to cancel the operation. Running the above example, we get the following results:

 $go run custom_cancelctx.go before f1: 1 after f1: 3 // 在context包中新创建了一个goroutine goroutine created by f1 exit

From this, we can see that in addition to the resource leakage that may be caused by the “business” level, there are also some resources in the implementation of cancel Context (such as the newly created goroutine above) that need to be released in time, otherwise “leakage” will also occur.

Some friends may ask such a question: in the called function (callee), should we continue to pass the original cancelCtx to the newly created goroutine, or create a new cancelCtx based on the parent cancelCtx and pass it to the goroutine? This reminds me of a problem I encountered during renovations: should I add valves in certain places in the water pipes?

With the valve, you can control the closing of all the way separately! Also in the code, creating a new cancelCtx based on the parent cancelCtx can do a separate cancellation operation without affecting the parentCtx, which depends on whether the business layer code needs to do this.

At this point, we have got the cancellation mechanism provided by the context package, but in practice, it is difficult for us to grasp the timing of the cancel Function call . To this end, the context package provides another useful control mechanism built on top of cancelCtx: timerCtx. Next, let’s take a look at timerCtx.

4) WithDeadline and WithTimeout functions

timerCtx provides a deadline-based cancellation control mechanism based on cancelCtx:

 type timerCtx struct { cancelCtx timer *time.Timer // Under cancelCtx.mu. deadline time.Time }

The context package provides two APIs for creating timerCtx: WithDeadline and WithTimeout functions:

 // $GOROOT/src/context/context.go func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { if parent == nil { panic("cannot create context from nil parent") } if cur, ok := parent.Deadline(); ok && cur.Before(d) { // The current deadline is already sooner than the new one. return WithCancel(parent) } 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) } } func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) }

From the implementation point of view, WithTimeout is the repackage of WithDeadline! We can understand WithDeadline. From the implementation of WithDeadline, this function sets a timer through time.AfterFunc, and the execution logic after the timer fires is to execute the cancel Function of the ctx. That is to say, timerCtx supports both manual cancel (the original cancelCtx mechanism) and timed cancel, and the cancel is usually completed by the timer.

With the basis of cancelCtx, timerCtx is not difficult to understand. Don’t pay attention, even if there is a timer to cancel the operation, we should not forget to explicitly call the cancel function returned by WithDeadline and WithTimeout. Isn’t it better to release resources early!

4. Summary

This article briefly explains the problems to be solved by the context package of the Go standard library, the composition of the context package, and the principles of value transfer and transfer control. I believe that after reading these contents, you will go back and look at the code you have written that uses the context package. will have a deeper understanding.

The context package is currently widely used in the Go ecosystem. Typically, the value information is passed in the http handler, and the tracing information is integrated through the trace ID in the context in the tracing framework.

The Go community’s voices on the context package are not all positive. The “virus-like” infectivity of context.Context is the aspect that has been criticized. The Go official also has an issue that records the feedback and optimization suggestions of the Go community on the context package. Interested partners can go to it.

The source code of the context package in this article is from Go version 1.19.1 , and may be different from older versions of Go or future versions of Go.

The source code for this article can be downloaded here .

5. References

  • Context Package Documentation Manual – https://ift.tt/6MaK2k8
  • Go Concurrency Patterns: Context – https://ift.tt/jiLCyUg

“Gopher Tribe” Knowledge Planet aims to create a high-quality Go learning and advanced community! High-quality first published Go technical articles, “three-day” first published reading rights, analysis of the current situation of Go language development twice a year, reading the fresh Gopher daily 1 hour in advance every day, online courses, technical columns, book content preview, must answer within 6 hours Guaranteed to meet all your needs about the Go language ecosystem! In 2022, the Gopher tribe will be fully revised, and will continue to share knowledge, skills and practices in the Go language and Go application fields, and add many forms of interaction. Everyone is welcome to join!

img{512x368}

img{512x368}

img{512x368}

img{512x368}

I love texting : Enterprise-level SMS platform customization development expert https://51smspush.com/. smspush : A customized SMS platform that can be deployed within the enterprise, with three-network coverage, not afraid of large concurrent access, and can be customized and expanded; the content of the SMS is determined by you, no longer bound, with rich interfaces, long SMS support, and optional signature. On April 8, 2020, China’s three major telecom operators jointly released the “5G Message White Paper”, and the 51 SMS platform will also be newly upgraded to the “51 Commercial Message Platform” to fully support 5G RCS messages.

The famous cloud hosting service provider DigitalOcean released the latest hosting plan. The entry-level Droplet configuration is upgraded to: 1 core CPU, 1G memory, 25G high-speed SSD, and the price is 5$/month. Friends who need to use DigitalOcean can open this link : https://ift.tt/0w82mHx to open your DO host road.

Gopher Daily Archive Repository – https://ift.tt/RC6UrcN

my contact information:

  • Weibo: https://ift.tt/Hp5qDyv
  • Blog: tonybai.com
  • github: https://ift.tt/Ya9ULKt

Business cooperation methods: writing, publishing books, training, online courses, partnership entrepreneurship, consulting, advertising cooperation.

© 2022, bigwhite . All rights reserved.

This article is reproduced from https://tonybai.com/2022/11/08/understand-go-context-by-example/
This site is for inclusion only, and the copyright belongs to the original author.