Home > Article > Backend Development > Detailed explanation of Context in Go language
Before Go 1.7, context was still uncompiled and existed in the golang.org/x/net/context package.
Later, the Golang team found that context was quite easy to use, so they incorporated context into the standard library in Go 1.7.
Context, also called 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 the Context interface has 4 methods
Deadline
: The first value returned is the deadline time. At this time point, the Context will automatically trigger the Cancel action. The second value returned is a Boolean value. true means that the deadline is set, false means that the deadline is not set. If the deadline is not set, the cancel function must be manually called to cancel the Context. Done
: Returns a read-only channel (will only be returned after being canceled) with type struct{}
. When this channel is readable, it means that the parent context has initiated a cancellation request. Based on this signal, the developer can perform some cleanup actions and exit the goroutine. Err
: Returns the reason why the context was canceled. Value
: Returns the value bound to the Context, which is a key-value pair, so the corresponding value can be obtained through a Key. This value is generally thread-safe. When a goroutine is opened, we cannot force it to close.
Common reasons for closing coroutines are as follows:
The first type is a normal shutdown and is not within the scope of today's discussion.
The second type is abnormal shutdown 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("监控退出,停止了...") return default: fmt.Println("goroutine监控中...") time.Sleep(2 * time.Second) } } }() time.Sleep(10 * time.Second) fmt.Println("可以了,通知监控停止") stop<- true //为了检测监控过是否停止,如果没有监控输出,就表示停止了 time.Sleep(5 * time.Second) }
In the example, we define a stop
chan to notify him End the background goroutine. The implementation is also very simple. In the background goroutine, use select to determine whether stop
can receive the value. If it can be received, it means that it can exit and stop; if it is not received, default will be executed. The monitoring logic in
will continue to monitor until the stop
notification is received.
The above is a goroutine scenario. What if there are multiple goroutines and multiple goroutine scenarios are opened under each goroutine? In Feixue Wuqing's blog about why to use Context, he said this:
The chan select method is a more elegant way to end a goroutine, but this method also has limitations. What if there are many goroutines that need to be controlled to end? What if these goroutines spawn other goroutines? What if there are endless layers of goroutines? This is very complicated. Even if we define many chans, it will be difficult to solve this problem, because the relationship chain of goroutine makes this scenario very complicated.
I don’t quite agree with what he said here, because I think even using only one channel can achieve the purpose of controlling (cancelling) multiple goroutines. Let’s use an example to verify it.
The principle of this example is: after using close to close the channel, if the channel is unbuffered, it will change from the original blocking to non-blocking, that is, readable, but the read will It is always zero, so based on this feature, you can determine whether the goroutine owning the channel should be closed.
package main import ( "fmt" "time" ) func monitor(ch chan bool, number int) { for { select { case v := <-ch: // 仅当 ch 通道被 close,或者有数据发过来(无论是true还是false)才会走到这个分支 fmt.Printf("监控器%v,接收到通道值为:%v,监控结束。\n", number,v) return default: fmt.Printf("监控器%v,正在监控中...\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) // 关闭所有 goroutine close(stopSingal) // 等待5s,若此时屏幕没有输出 <正在监控中> 就说明所有的goroutine都已经关闭 time.Sleep( 5 * time.Second) fmt.Println("主程序退出!!") }
The output is as follows
监控器4,正在监控中... 监控器1,正在监控中... 监控器2,正在监控中... 监控器3,正在监控中... 监控器5,正在监控中... 监控器2,接收到通道值为:false,监控结束。 监控器3,接收到通道值为:false,监控结束。 监控器5,接收到通道值为:false,监控结束。 监控器1,接收到通道值为:false,监控结束。 监控器4,接收到通道值为:false,监控结束。 主程序退出!!
The above example shows that when we define an unbuffered channel, if we want to close all goroutines, we can use close to close the channel, and then in all goroutines Continuously check whether the channel is closed (the premise is that you must make an agreement that you will only close the channel and not send other data, otherwise sending data once will close a goroutine, which will not meet our expectations, so it is best for you to This channel is encapsulated and restricted) to decide whether to end the goroutine.
So as you see here, as a beginner, I still haven't found the inevitable reason to use Context. I can only say that Context is a very useful thing. Using it makes it easier for us to deal with some problems when dealing with concurrency. , but it is not indispensable.
In other words, it does not solve the problem of whether can, but the problem of better to use.
If you don’t use the above close channel method, is there any other more elegant way to achieve it?
Yes, that is what this article is about Context
I used Context to modify the above example.
package main import ( "context" "fmt" "time" ) func monitor(ctx context.Context, number int) { for { select { // 其实可以写成 case <- ctx.Done() // 这里仅是为了让你看到 Done 返回的内容 case v :=<- ctx.Done(): fmt.Printf("监控器%v,接收到通道值为:%v,监控结束。\n", number,v) return default: fmt.Printf("监控器%v,正在监控中...\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) // 关闭所有 goroutine cancel() // 等待5s,若此时屏幕没有输出 <正在监控中> 就说明所有的goroutine都已经关闭 time.Sleep( 5 * time.Second) fmt.Println("主程序退出!!") }
The key code here is only three lines
第一行:以 context.Background() 为 parent context 定义一个可取消的 context
ctx, cancel := context.WithCancel(context.Background())
第二行:然后你可以在所有的goroutine 里利用 for + select 搭配来不断检查 ctx.Done() 是否可读,可读就说明该 context 已经取消,你可以清理 goroutine 并退出了。
case <- ctx.Done():
第三行:当你想到取消 context 的时候,只要调用一下 cancel 方法即可。这个 cancel 就是我们在创建 ctx 的时候返回的第二个值。
cancel()
运行结果输出如下。可以发现我们实现了和 close 通道一样的效果。
监控器3,正在监控中... 监控器4,正在监控中... 监控器1,正在监控中... 监控器2,正在监控中... 监控器2,接收到通道值为:{},监控结束。 监控器5,接收到通道值为:{},监控结束。 监控器4,接收到通道值为:{},监控结束。 监控器1,接收到通道值为:{},监控结束。 监控器3,接收到通道值为:{},监控结束。 主程序退出!!
创建 Context 必须要指定一个 父 Context,当我们要创建第一个Context时该怎么办呢?
不用担心,Go 已经帮我们实现了2个,我们代码中最开始都是以这两个内置的context作为最顶层的parent context,衍生出更多的子Context。
var ( background = new(emptyCtx) todo = new(emptyCtx) ) func Background() Context { return background } func TODO() Context { return todo }
一个是Background,主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context,它不能被取消。
一个是TODO,如果我们不知道该使用什么Context的时候,可以使用这个,但是实际应用中,暂时还没有使用过这个TODO。
他们两个本质上都是emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。
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 }
上面在定义我们自己的 Context 时,我们使用的是 WithCancel
这个方法。
除它之外,context 包还有其他几个 With 系列的函数
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
这四个函数有一个共同的特点,就是第一个参数,都是接收一个 父context。
通过一次继承,就多实现了一个功能,比如使用 WithCancel 函数传入 根context ,就创建出了一个子 context,该子context 相比 父context,就多了一个 cancel context 的功能。
如果此时,我们再以上面的子context(context01)做为父context,并将它做为第一个参数传入WithDeadline函数,获得的子子context(context02),相比子context(context01)而言,又多出了一个超过 deadline 时间后,自动 cancel context 的功能。
接下来我会举例介绍一下这几种 context,其中 WithCancel 在上面已经讲过了,下面就不再举例了
package main import ( "context" "fmt" "time" ) func monitor(ctx context.Context, number int) { for { select { case <- ctx.Done(): fmt.Printf("监控器%v,监控结束。\n", number) return default: fmt.Printf("监控器%v,正在监控中...\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("监控器取消的原因: ", ctx02.Err()) } fmt.Println("主程序退出!!") }
输出如下
监控器5,正在监控中... 监控器1,正在监控中... 监控器2,正在监控中... 监控器3,正在监控中... 监控器4,正在监控中... 监控器3,监控结束。 监控器4,监控结束。 监控器2,监控结束。 监控器1,监控结束。 监控器5,监控结束。 监控器取消的原因: context deadline exceeded 主程序退出!!
WithTimeout 和 WithDeadline 使用方法及功能基本一致,都是表示超过一定的时间会自动 cancel context。
唯一不同的地方,我们可以从函数的定义看出
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithDeadline 传入的第二个参数是 time.Time 类型,它是一个绝对的时间,意思是在什么时间点超时取消。
而 WithTimeout 传入的第二个参数是 time.Duration 类型,它是一个相对的时间,意思是多长时间后超时取消。
package main import ( "context" "fmt" "time" ) func monitor(ctx context.Context, number int) { for { select { case <- ctx.Done(): fmt.Printf("监控器%v,监控结束。\n", number) return default: fmt.Printf("监控器%v,正在监控中...\n", number) time.Sleep(2 * time.Second) } } } func main() { ctx01, cancel := context.WithCancel(context.Background()) // 相比例子1,仅有这一行改动 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("监控器取消的原因: ", ctx02.Err()) } fmt.Println("主程序退出!!") }
输出的结果和上面一样
监控器1,正在监控中... 监控器5,正在监控中... 监控器3,正在监控中... 监控器2,正在监控中... 监控器4,正在监控中... 监控器4,监控结束。 监控器2,监控结束。 监控器5,监控结束。 监控器1,监控结束。 监控器3,监控结束。 监控器取消的原因: context deadline exceeded 主程序退出!!
通过Context我们也可以传递一些必须的元数据,这些数据会附加在Context上以供使用。
元数据以 Key-Value 的方式传入,Key 必须有可比性,Value 必须是线程安全的。
还是用上面的例子,以 ctx02 为父 context,再创建一个能携带 value 的ctx03,由于他的父context 是 ctx02,所以 ctx03 也具备超时自动取消的功能。
package main import ( "context" "fmt" "time" ) func monitor(ctx context.Context, number int) { for { select { case <- ctx.Done(): fmt.Printf("监控器%v,监控结束。\n", number) return default: // 获取 item 的值 value := ctx.Value("item") fmt.Printf("监控器%v,正在监控 %v \n", 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("监控器取消的原因: ", ctx02.Err()) } fmt.Println("主程序退出!!") }
输出如下
监控器4,正在监控 CPU 监控器5,正在监控 CPU 监控器1,正在监控 CPU 监控器3,正在监控 CPU 监控器2,正在监控 CPU 监控器2,监控结束。 监控器5,监控结束。 监控器3,监控结束。 监控器1,监控结束。 监控器4,监控结束。 监控器取消的原因: context deadline exceeded 主程序退出!!
推荐教程:《Go教程》
The above is the detailed content of Detailed explanation of Context in Go language. For more information, please follow other related articles on the PHP Chinese website!