概述
context主要用来在goroutine之间传递上下文信息 ,包括:取消信号、超时时间、截止时间、k-v等。
在Goroutine构成的树形结构中对信号进行同步以减少计算资源的浪费 是context.Context
的最大作用。
那其实很直观的就会有个疑问,Go中怎么通过传递ctx实现超时控制的呢?我们可以看下面两个示例:
示例:系统代码使用ctx
这段代码来源于src/database/sql.go
,大致在1336行。
1
2
3
4
5
6
7
8
9
10
// 阻塞从req中获取链接,如果超时,直接返回
select {
case <- ctx . Done ():
// 获取链接超时了,直接返回错误
// do something
return nil , ctx . Err ()
case ret , ok := <- req :
// 拿到链接,校验并返回
return ret . conn , ret . err
}
官方示例WebSearch
示例链接:https://go.dev/blog/context
示例中将代码分成了三部分:
server
主调
userip
用来给ctx传递KV做示例
google
用来展示使用ctx进行超时控制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// google
func httpDo ( ctx context . Context , req * http . Request , f func ( * http . Response , error ) error ) error {
// Run the HTTP request in a goroutine and pass the response to f.
c := make ( chan error , 1 )
req = req . WithContext ( ctx )
go func () { c <- f ( http . DefaultClient . Do ( req )) }()
select {
case <- ctx . Done ():
<- c // Wait for f to return.
return ctx . Err ()
case err := <- c :
return err
}
}
可以看到,在处理超时或者取消这部分都是通过select
语句来检测ctx.Done()返回的channel,如果ctx先取消的话,则可以检测上下文取消的原因,然后返回结果。我们还可以接着用一个示例来具体描述它们的行为:
示例:具体示例说明取消
我们这个程序发送一个ping请求到baidu.com,并设置20ms的超时。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main
import (
"context"
"fmt"
"log"
"time"
probing "github.com/prometheus-community/pro-bing"
)
func main () {
pinger , err := probing . NewPinger ( "baidu.com" )
if err != nil {
log . Fatalln ( err )
}
pinger . Count = 1
pinger . OnRecv = func ( pkt * probing . Packet ) {
fmt . Printf ( "%d bytes from %s: icmp_seq=%d time=%v\n" ,
pkt . Nbytes , pkt . IPAddr , pkt . Seq , pkt . Rtt )
}
pinger . OnFinish = func ( stats * probing . Statistics ) {
fmt . Printf ( "\n--- %s ping statistics ---\n" , stats . Addr )
fmt . Printf ( "%d packets transmitted, %d packets received, %v%% packet loss\n" ,
stats . PacketsSent , stats . PacketsRecv , stats . PacketLoss )
fmt . Printf ( "round-trip min/avg/max/stddev = %v/%v/%v/%v\n" ,
stats . MinRtt , stats . AvgRtt , stats . MaxRtt , stats . StdDevRtt )
}
ctx , _ := context . WithTimeout ( context . Background (), time . Millisecond * 20 )
err = pinger . RunWithContext ( ctx ) // block until finished
if err != nil {
log . Fatalln ( err )
}
}
由于大部分的情况下,这个延时都会超过20ms,于是我们大概率会得到下面的运行结果:
运行超时的结果
但是,于此同时,我们通过tcpdump监听的结果可以发现,实际上是收到了ping的结果:
tcpdump的结果
内部实现
数据结构
内部定义了一个context.Context
的接口,该接口定义了4个方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Context interface {
// Deadline 返回 context.Context 被取消的时间,也就是完成工作的截止日期
// 若没有设置deadline,则ok == false
Deadline () ( deadline time . Time , ok bool )
// Done 返回一个 Channel,这个Channel会在当前工作完成或者上下文被取消后关闭,
// 多次调用Done方法会返回同一个Channel;
// 当然,在该上下文永远不可能被取消的情况下,它也可能返回nil.
Done () <- chan struct {}
// Err 返回context.Context结束的原因,它只会在Done方法对应的Channel关闭时返回非空的值
Err () error
// Value 从context.Context中获取键对应的值,对于同一个上下文来说,
// 多次调用Value并传入相同的Key会返回相同的结果,该方法可以用来传递请求特定的数据
Value ( key any ) any
}
取消信号
context.WithCancel
函数能从context.Context
中衍生出一个新的子上下文并返回用于取消该上下文的函数。一旦我们执行返回的取消函数,当前上下文以及它的子上下文都会被取消,所有的Goroutine都会同步收到这一取消信号。
对应的源码如下:
1
2
3
4
5
func WithCancel ( parent Context ) ( ctx Context , cancel CancelFunc ) {
c := newCancelCtx ( parent ) // 将传入的上下文包装成私有结构体: context.cancelCtx
propagateCancel ( parent , & c ) // 构建父子上下文之间的关联,当父上下文被取消时,子上下文也会被取消
return & c , func () { c . cancel ( true , Canceled ) }
}
定时取消
除了context.WithCancel
之外,context
包中的另外两个函数context.WithDeadline
和context.WithTimeout
也都能创建可以被取消的计时器上下文context.timerCtx
。
1
2
3
func WithTimeout ( parent Context , timeout time . Duration ) ( Context , CancelFunc ) {
return WithDeadline ( parent , time . Now (). Add ( timeout ))
}
常用方法
With系列函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 传递一个父Context作为参数,返回子Context,以及一个取消函数用来取消Context。
func WithCancel ( parent Context ) ( ctx Context , cancel CancelFunc )
// 和WithCancel差不多,它会多传递一个截止时间参数,意味着到了这个时间点,会自动取消Context,
// 当然我们也可以不等到这个时候,可以提前通过取消函数进行取消。
func WithDeadline ( parent Context , deadline time . Time ) ( Context , CancelFunc )
// WithTimeout和WithDeadline基本上一样,这个表示是超时自动取消,是多少时间后自动取消Context的意思
func WithTimeout ( parent Context , timeout time . Duration ) ( Context , CancelFunc )
// WithValue函数和取消Context无关,它是为了生成一个绑定了一个键值对数据的Context,
// 绑定的数据可以通过Context.Value方法访问到,这是我们实际用经常要用到的技巧,一般我们想要通过上下文来传递数据时,可以通过这个方法,
// 如我们需要tarce追踪系统调用栈的时候。
func WithValue ( parent Context , key , val interface {}) Context
参考文献
Go语言设计与实现——上下文context
详解golang中的context
Go Concurrency Patterns: Context