Why
很多框架中接口函数第一个参数统一是ctx context.Context接口,如net/http中conn.serve(ctx context.Context)方法,为什么要这么设计呢?
因为一般一个网络请求Request,会在多个Goroutine中处理,而这些Goroutine可能需要共享Request的一些信息;同时当Request被取消或者超时的时候,所有从这个Request创建的所有Goroutine也应该被结束。上下文则几乎已经成为传递与请求同生存周期变量的标准方法。
What
context用于Goroutine之间共享状态变量,另一个gorutine通过设置ctx变量值,传递过期或撤销信号给被调用的程序单元。
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
- Deadline会返回一个超时时间,Goroutine获得了超时时间后,例如可以对某些io操作设定超时时间。
- Done方法返回一个信道(channel),传递是否已关闭的信号。
- Err方法表明Context被撤的原因。
- Value可以让Goroutine共享一些数据,当然获得数据是协程安全的。
How
Goroutine的创建和调用关系总是像层层调用的,而更靠顶部的Goroutine应有办法主动关闭其下属的Goroutine。
Context结构也应该像一棵树,要创建Context树,第一步就是要得到根节点,context.Background函数的返回值就是根节点。
创建子context 函数 |
解释 |
---|---|
|
该Context一般由接收请求的第一个Goroutine创建,即根节点,一般返回emptyCtx |
|
创建子context,返回取消方法,父Goroutine可以调用取消子goroutine |
|
创建子context,到了dealine或者被父gorutine调用返回的取消方法,终止 |
|
创建子context,调用时间过了timeout或者被父gorutine调用返回的取消方法,终止 WithDeadline(parent, time.Now().Add(timeout)) |
|
返回valueCtx,传递了kv对到子context |
子节点需要类似如下代码来接收是否已结束,并退出该Goroutine:
select {
case <-cxt.Done():
// do some clean...
}
Example
ctx := context.WithValue(baseCtx, ServerContextKey, srv) // 服务创建子context
for {
rw, err := l.Accept() // 接收请求
connCtx := ctx
c := srv.newConn(rw)
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve(connCtx) // 子routine传入子ctx
}
Summary
context通过构建树型关系的Context,达到上一层Goroutine给下层Goroutine
父子传递控制信号&共享变量
- Context对象生存周期一般仅为一个请求的处理周期。即对一个请求创建一个Context变量(它为Context树结构的根);在请求处理结束后,撤销此ctx变量,释放资源。
- 每次创建Goroutine,可将原Context传递给Goroutine,也可创建一个子Context
- Context能存储不同类型、不同数目的值KV,Goroutines安全读写。
- 当通过父Context对象创建子Context对象时,可同时获得子Context的一个撤销函数,这样父Context对象的创建环境就获得了对子Context将要被传递到的Goroutine的撤销权。
使用原则
- 不作为结构体字段,按需显式地函数间传参,作为第一个参数使用,一般命名为ctx;
- 不要传入一个nil的Context,如果你不确定你要用什么Context的时候传一个context.TODO;
- 共享变量Values只用于请求scope的数据,不传可选的参数;
- 同个Context可传到不同的goroutine中,在多个goroutine中是线程安全的
- 在子Context被传递到的goroutine中,应该对该子Context的Done信道(channel)进行监控,一旦该信道被关闭,应主动终止对当前请求信息的处理,释放资源并返回。