Deep understanding of context in go language

Time:2020-8-9

Hi, everyone. This is Mingo.

During my time of learning Golang, I wrote detailed study notes on my personal WeChat official account “Go programming time”. For Go language, I am also a beginner, so writing things should be more suitable for students who are just in contact. If you are just learning Go language, do not pay close attention to it, learn together and grow together.

My online blog: http://golang.iswbm.com
My GitHub: github.com/iswbm/GolangCodingTime

1. What is context?

Before go 1.7, context was still uncommitted and existed in golang.org/x/net/context In the bag.

Later, the golang team found that context was still very useful, so it incorporated the context into the standard library in go 1.7.

Context, also known as context, its interface is defined as follows


type Context interface {
 Deadline() (deadline time.Time, ok bool)
 Done() <-chan struct{}
 Err() error
 Value(key interface{}) interface{}
}

You can see that there are four methods in the context interface

  • Deadline: the first value returned is the deadline. At this time point, the context will automatically trigger the cancel action. The second value returned is a Boolean value. True indicates that the deadline has been set, and false means that no deadline has been set. If no deadline has been set, manually call the cancel function to cancel the context.
  • Done: returns a read-only channel (only returned after being cancelled), of typestruct{}。 When the channel is readable, it means that the parent context has initiated a cancellation request. According to this signal, the developer can perform some cleaning actions and exit goroutine.
  • Err: returns the reason why context was canceled.
  • Value: returns the value bound to context, which is a key value pair, so the corresponding value can be obtained through a key. This value is generally thread safe.

2. Why context is needed?

When a coroutine is opened, we cannot force it to be closed.

The common reasons for shutting down the process are as follows:

  • Goroutine runs by herself and exits
  • The main process crash exits and goroutine is forced to exit
  • The signal is sent through the channel to guide the coprocessor to close.

The first one, which belongs to normal shutdown, is not within the scope of today’s discussion.

Second, it belongs to abnormal closing, and the code should be optimized.

The third method is that developers can manually control the coroutine. The code example is as follows:

func main() {
 stop := make(chan bool)

 go func() {
 for {
 select {
 case <-stop:
 fmt.Println ("monitoring exited, stopped...)
 return
 default:
 fmt.Println ("goroutine monitoring...")
 time.Sleep(2 * time.Second)
 }
 }
 }()

 time.Sleep(10 * time.Second)
 fmt.Println ("OK, inform the monitor to stop")
 stop<- true
 //In order to detect whether monitoring has stopped, if there is no monitoring output, it means that it has stopped
 time.Sleep(5 * time.Second)

}

In the example, we define astopChan, tell him to end goroutine. The implementation is also very simple. In the background goroutine, select is used to judgestopWhether the value can be received. If it can be received, it means that it can exit and stop; if not, it will be executeddefaultThe monitoring logic in. Continue to monitor until you receive itstopNotice.

The above is a goroutine scenario. If there are multiple goroutines, how about multiple goroutines under each goroutine? In Feixue’s ruthless blog, he said this about why context should be used

Chan + select is a more elegant way to end a goroutine. However, this method also has limitations. What if there are many goroutines that need to control the end? What if these goroutines lead to other goroutines? What if there are endless goroutines? This is very complicated, even if we define many Chan, it is difficult to solve this problem, because goroutine’s relationship chain leads to such a complex scenario.

I don’t quite agree with what he said here, because I think that even if only one channel is used, the goal of controlling (canceling) multiple goroutines can be achieved. The following is an example to verify.

The principle of this example is: after closing a channel with close, if the channel is unbuffered, it will change from blocking to non blocking, that is to say, it can be read, but the read value will always be zero. Therefore, according to this feature, we can judge whether the goroutine that owns the channel should be closed.

package main

import (
 "fmt"
 "time"
)

func monitor(ch chan bool, number int) {
 for {
 select {
 case v := <-ch:
 //Only when the ch channel is closed or data is sent (whether true or false) will it go to this branch
 fmt.Printf ("monitor% v, received channel value of% v, end of monitoring. \n", number,v)
 return
 default:
 fmt.Printf ("monitor% v, monitoring... \ n", number)
 time.Sleep(2 * time.Second)
 }
 }
}

func main() {
 stopSingal := make(chan bool)

 for i :=1 ; i <= 5; i++ {
 go monitor(stopSingal, i)
 }

 time.Sleep( 1 * time.Second)
 //Close all goroutines
 close(stopSingal)

 //Wait for 5 seconds. If the screen does not output "under monitoring", all goroutines have been closed
 time.Sleep( 5 * time.Second)

 fmt.Println (the main program exits!! ""

}

The output is as follows

Monitor 4, monitoring
Monitor 1, monitoring
Monitor 2, monitoring
Monitor 3, monitoring
Monitor 5, monitoring
Monitor 2, receive the channel value as false, and the monitoring is finished.
Monitor 3, receive the channel value as false, and the monitoring ends.
Monitor 5, receive the channel value as false, and the monitoring is finished.
Monitor 1, receive the channel value as false, and the monitoring is finished.
Monitor 4, receive the channel value as false, and the monitoring ends.
Main program exit!!

The above example shows that when we define a bufferless channel, if we want to close all goroutines, we can use close to close the channel, and then constantly check whether the channel is closed in all goroutines (provided that you have agreed that the channel will only be closed) It will not send other data, otherwise sending data once will close a goroutine, which will not meet our expectations. Therefore, it is better to make a further layer of encapsulation on this channel to determine whether to terminate goroutine.

So you can see here, as a beginner, I still haven’t found the necessary reason to use context. I can only say that context is a good thing to use. It is convenient for us to deal with some problems in concurrency, but it is not indispensable.

In other words, it solves not the problem of whether it can be used, but the problem of better use.

3. Simply use context

If you don’t use the above close channel, is there any other more elegant way to achieve it?

Yes, that is the context of this article

I modified the above example with context.

package main

import (
 "context"
 "fmt"
 "time"
)

func monitor(ctx context.Context, number int) {
 for {
 select {
 //It can be written as a case<- ctx.Done ()
 //It's just for you to see what done returned
 case v :=<- ctx.Done():
 fmt.Printf ("monitor% v, received channel value of% v, end of monitoring. \n", number,v)
 return
 default:
 fmt.Printf ("monitor% v, monitoring... \ n", number)
 time.Sleep(2 * time.Second)
 }
 }
}

func main() {
 ctx, cancel := context.WithCancel(context.Background())

 for i :=1 ; i <= 5; i++ {
 go monitor(ctx, i)
 }

 time.Sleep( 1 * time.Second)
 //Close all goroutines
 cancel()

 //Wait for 5 seconds. If the screen does not output "under monitoring", all goroutines have been closed
 time.Sleep( 5 * time.Second)

 fmt.Println (the main program exits!! ""

}

The key code in this is only three lines

Line 1: with context.Background () define a cancelable context for parent context

ctx, cancel := context.WithCancel(context.Background())

Line 2: then you can use the for + select collocation in all goroutines to keep checking ctx.Done () whether it is readable or not means that the context has been canceled. You can clean up goroutine and exit.

case <- ctx.Done():

Line 3: when you want to cancel context, just call the cancel method. This cancel is the second value we return when we create CTX.

cancel()

The output of running results is as follows. We can see that we achieve the same effect as the close channel.

Monitor 3, monitoring
Monitor 4, monitoring
Monitor 1, monitoring
Monitor 2, monitoring
Monitor 2, received channel value: {}, monitoring end.
Monitor 5, received channel value: {}, monitoring end.
Monitor 4, received channel value: {}, monitoring end.
Monitor 1, received channel value: {}, monitoring end.
Monitor 3, received channel value: {}, monitoring end.
Main program exit!!

4. What is the root context?

To create a context, you must specify a parent context. What should we do when we want to create the first context?

Don’t worry, go has helped us implement two. At the beginning of our code, these two built-in contexts are used as the top-level parent context to derive more sub contexts.


var (
 background = new(emptyCtx)
 todo = new(emptyCtx)
)

func Background() Context {
 return background
}

func TODO() Context {
 return todo
}

One is background, which is mainly used in the main function, initialization and test code. As the top-level context of the tree structure, that is, the root context, it cannot be cancelled.

One is todo. If we don’t know what context to use, we can use it. However, in practical application, we have not used this todo yet.

Both of them are essentially emptyctx structure types, which can’t be cancelled, have no deadline set, and do not carry any values.


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
}

5. Inheritance and derivation of context

When defining our own context, we usedWithCancelThis method.

In addition to it, the context package has several other functions of the with series


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

The four functions have a common feature, that is, the first parameter receives a parent context.

Through one inheritance, one more function is realized. For example, if the withcancel function is used to pass in the root context, a child context is created. Compared with the parent context, the child context has one more function of cancel context.

If we take the above child context (context01) as the parent context and pass it as the first parameter into the withdeadline function, the obtained child context (context02) has an additional function of automatically canceling context after exceeding the deadline time compared with the child context (context01).

Next, I’ll give an example of these contexts. With cancel has already been mentioned above, and I won’t give any more examples below

Example 1: with deadline

package main

import (
 "context"
 "fmt"
 "time"
)

func monitor(ctx context.Context, number int) {
 for {
 select {
 case <- ctx.Done():
 fmt.Printf ("monitor% v, end of monitoring. \n", number)
 return
 default:
 fmt.Printf ("monitor% v, monitoring... \ n", number)
 time.Sleep(2 * time.Second)
 }
 }
}

func main() {
 ctx01, cancel := context.WithCancel(context.Background())
 ctx02, cancel := context.WithDeadline(ctx01, time.Now().Add(1 * time.Second))

 defer cancel()

 for i :=1 ; i <= 5; i++ {
 go monitor(ctx02, i)
 }

 time.Sleep(5 * time.Second)
 if ctx02.Err() != nil {
 fmt.Println ("reason for monitor cancellation:" ctx02. Err())
 }

 fmt.Println (the main program exits!! ""
}

The output is as follows

Monitor 5, monitoring
Monitor 1, monitoring
Monitor 2, monitoring
Monitor 3, monitoring
Monitor 4, monitoring
Monitor 3, monitor over.
Monitor 4, end of monitoring.
Monitor 2, end of monitoring.
Monitor 1, end of monitoring.
Monitor 5, end of monitoring.
Reason for monitor cancellation: context deadline exceeded
Main program exit!!

Example 2: withtimeout

The use methods and functions of withtimeout and withdeadline are basically the same, which means that context will be automatically cancelled after a certain period of time.

The only difference, we can see from the definition of the function


func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

The second parameter passed in with deadline is time.Time Type, which is an absolute time, means at what time the timeout is cancelled.

The second parameter passed in by withtimeout is time.Duration Type, which is a relative time, means how long after the timeout to cancel.

package main

import (
 "context"
 "fmt"
 "time"
)

func monitor(ctx context.Context, number int) {
 for {
 select {
 case <- ctx.Done():
 fmt.Printf ("monitor% v, end of monitoring. \n", number)
 return
 default:
 fmt.Printf ("monitor% v, monitoring... \ n", number)
 time.Sleep(2 * time.Second)
 }
 }
}

func main() {
 ctx01, cancel := context.WithCancel(context.Background())
 
 //Compared with example 1, only this line has been changed
 ctx02, cancel := context.WithTimeout(ctx01, 1* time.Second)

 defer cancel()

 for i :=1 ; i <= 5; i++ {
 go monitor(ctx02, i)
 }

 time.Sleep(5 * time.Second)
 if ctx02.Err() != nil {
 fmt.Println ("reason for monitor cancellation:" ctx02. Err())
 }

 fmt.Println (the main program exits!! ""
}

The output is the same as above

 Monitor 1, monitoring
Monitor 5, monitoring
Monitor 3, monitoring
Monitor 2, monitoring
Monitor 4, monitoring
Monitor 4, end of monitoring.
Monitor 2, end of monitoring.
Monitor 5, end of monitoring.
Monitor 1, end of monitoring.
Monitor 3, monitor over.
Reason for monitor cancellation: context deadline exceeded
Main program exit!!

Example 3: withvalue

Through the context, we can also pass some necessary metadata, which will be attached to the context for use.

Metadata is passed in as key value. Key must be comparable and value must be thread safe.

In the above example, ctx02 is used as the parent context to create a ctx03 that can carry value. Because its parent context is ctx02, ctx03 also has the function of automatically canceling timeout.

package main

import (
 "context"
 "fmt"
 "time"
)

func monitor(ctx context.Context, number int) {
 for {
 select {
 case <- ctx.Done():
 fmt.Printf ("monitor% v, end of monitoring. \n", number)
 return
 default:
 //Gets the value of item
 value := ctx.Value("item")
 fmt.Printf ("monitor% v, monitoring% v, number, value)"
 time.Sleep(2 * time.Second)
 }
 }
}

func main() {
 ctx01, cancel := context.WithCancel(context.Background())
 ctx02, cancel := context.WithTimeout(ctx01, 1* time.Second)
 ctx03 := context.WithValue(ctx02, "item", "CPU")

 defer cancel()

 for i :=1 ; i <= 5; i++ {
 go monitor(ctx03, i)
 }

 time.Sleep(5 * time.Second)
 if ctx02.Err() != nil {
 fmt.Println ("reason for monitor cancellation:" ctx02. Err())
 }

 fmt.Println (the main program exits!! ""
}

The output is as follows

Monitor 4, monitoring CPU
Monitor 5, monitoring CPU
Monitor 1, monitoring CPU
Monitor 3, monitoring CPU
Monitor 2, monitoring CPU
Monitor 2, end of monitoring.
Monitor 5, end of monitoring.
Monitor 3, monitor over.
Monitor 1, end of monitoring.
Monitor 4, end of monitoring.
Reason for monitor cancellation: context deadline exceeded
Main program exit!!

6. Matters needing attention in using context

  • Generally, context is passed as the first parameter of a function (normative practice), and the variable name is recommended to be called CTX
  • Context is thread safe and can be safely used in multiple goroutines.
  • When you pass the context to multiple goroutines for use, all goroutines can receive a cancellation signal as long as you execute the cancel operation
  • Do not pass variables that can be passed by function parameters to the value of context.
  • When a function needs to receive a context, but you don’t know what context to pass, you can use the context.TODO Instead of passing a nil.
  • When a context is cancelled, all child contexts inherited from the context are cancelled.

summary

This article on the in-depth understanding of the go language context on this, more related to go language context content, please search the previous articles of developeppaer or continue to browse the relevant articles below, I hope you will support developeppaer more in the future!