go语言之context
context为go自带的包,在安装路径下的src/context文件夹中
在 Go http包的Server中,每一个请求在都有一个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和RPC服务。用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源,因此, Context 专门用来简化 对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用。
Context 又被称为上下文,与 WaitGroup 不同的是,context 对于 goroutine 有更强的控制力,可以管理多级的 goroutine,可以简单理解为以下几点:
- 当前协程取消了,可以通知所有由它创建的子协程退出
- 当前协程取消了,不会影响到创建它的父级协程的状态
- 扩展了额外的功能:超时取消、定时取消、可以和子协程共享数据
一、Context控制goroutine(取消)
1、下面例子通过context控制单个goroutine,如图:

- 第22行:context.Background() 返回一个空的Context,这个空的Context一般作为整个Context树的根节点,然后使用context.WithCancel(parent)函数,创建一个可取消的子Context,然后当作参数传给goroutine使用,这样就可以使用这个子Context跟踪这个goroutine
- 第23行:并发执行test函数,函数参数为Context接口,函数中通过for执行无限循环,内部通过select判断是否要结束,如果ctx.Done()接收到了值,说明goroutine结束,如果接收不到,继续运行
- 第24行:休眠6秒,可以理解为test函数运行了6s,然后调用cancel函数停止goroutine
- 第25行:执行取消函数,可以理解为go test执行6s中后再执行cancel(),它是CancelFunc类型的,也是WithCancel返回值,通过调用它就可以发出取消指令
运行结果如下:

如果不通过cancel手动来停止goroutine,我们看下会发生什么,把cancel注释,如图:

运行结果如下:

从上图可以看出,如果不通过cancel手动取消goroutine,那么go test会持续运行到主函数main自带的goroutine结束后一起退出
2、下面例子通过context控制多个goroutine,如图:

运行结果如下:

上面例子中启动了3个goroutine,每一个都使用了Context进行跟踪,当我们使用cancel函数通知取消时,这3个goroutine都会被结束,Context就像一个控制器一样,按下取消开关后,基于这个Context或者衍生的子Context都会收到通知,进行清理操作,最终释放goroutine,从而解决了goroutine启动后不可控以及占用资源的问题
二、Context接口(go版本v1.18.1)
Context的接口的方法如下:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
- Deadline方法是获取设置的截止时间的意思,第一个返回式是截止时间,到了这个时间点,Context会自动发起取消请求;第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消
- Done方法返回一个只读的chan,类型为struct{},我们在goroutine中,如果该方法返回的chan可以读取,则意味着parent context已经发起了取消请求,我们通过Done方法收到这个信号后,就应该做清理操作,然后退出goroutine,释放资源
- Err方法返回取消的错误原因,因为什么Context被取消
- Value方法获取该Context上绑定的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的
上面方法中最常用的就是Done(),如果Context取消的时候,我们就可以得到一个关闭的chan,关闭的chan是可以读取的,所以只要可以读取的时候,就意味着收到Context取消的信号了,用法可参考上面例子
Context接口并不需要我们实现,Go内置已经帮我们实现了2个(安装路径下/src/context/context.go文件),代码中最开始都是以这两个内置的作为最顶层的partent context,衍生出更多的子Context,如图:

- emptyCtx实现了Context中的四个方法,因此实现了Context接口
- Background,主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context
- TODO,它目前还不知道具体的使用场景,如果我们不知道该使用什么Context的时候,可以使用这个
两个本质上都是emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context
下面是emptyCtx实现Context接口的方法,可以看到,这些方法返回都是nil或者零值,如图:

三、Context的继承衍生
有了上面的根Context,如何衍生更多的子Context?此时需要依靠context中的with系列函数,如下:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val any) Context
- WithCancel:传递一个父Context作为参数,返回子Context,以及一个取消函数用来取消Context
- WithDeadline:和WithCancel差不多,它会多传递一个截止时间参数,意味着到了这个时间点,会自动取消Context,当然也可以不等到这个时候,提前通过取消函数进行取消
- WithTimeout:和WithDeadline基本上一样,这个表示是超时自动取消,是多少时间后自动取消Context的意思
- WithValue:和取消Context无关,它是为了生成一个绑定了一个键值对数据的Context,这个绑定的数据可以通过Context.Value方法访问到
通过上面这些函数就可以创建一颗Context树,树的每个节点可以有任意多个子节点,节点层级可以有多个,上面四个with函数,第一个接收参数都是partent参数,也就是父Context,我们需要基于父Context创建出子Context,也可以理解为基于父Context的衍生
前三个函数都返回一个取消函数CancelFunc,这是一个函数类型,定义如下:
type CancelFunc func()
这就是取消函数的类型,该函数可以取消一个Context,以及这个节点Context下所有的所有的Context,不管有多少层级
四、通过WithValue传递元数据

- 22行:定义key的名字为name,key为自定义的值,写成key100也可以
- 26行:通过WithValue添加K-V键值对
- 16行:通过Value()方法读取key的值
运行结果如下:

注:使用WithValue传值,一般是必须的值,不要什么值都传递
Context 使用原则:
- 不要把Context放在结构体中,要以参数的方式传递
- 以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位
- 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO
- Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递
- Context是线程安全的,可以放心的在多个goroutine中传递
五、Context实现超时功能:
1、下面例子创建子节点的context,5s后结束,如图:

- 25行:创建过期时间为5s
- 26行:根据根节点context创建子节点context,并设置超时时间为5s
- 27:执行并发程序,通过ctx跟踪,因此执行5s后将收到取消通知
运行结果如下:

从上图看出,并发程序运行5s后收到了取消通知,因此执行ctx.Done()对应的case语句


