WHY
每一个长请求都应该有个超时限制
需要在调用中传递这个超时
比如开始处理请求的时候我们说是 3 秒钟超时
那么在函数调用中间,这个超时还剩多少时间了?
需要在什么地方存储这个信息,这样请求处理中间可以停止
context
Context通常被译作上下文。
一般理解为程序单元的一个运行状态、现场、快照,而翻译中上下又很好地诠释了其本质,上下上下则是存在上下层的传递,上会把内容传递给下。在Go语言中,程序单元也就指的是Goroutine。
为了支持多Goroutine管理机制。
每个Goroutine在执行之前,都要先知道程序当前的执行状态,通常将这些执行状态封装在一个Context变量中,传递给要执行的Goroutine中。上下文则几乎已经成为传递与请求同生存周期变量的标准方法。在网络编程下,当接收到一个网络请求Request,处理Request时,我们可能需要开启不同的Goroutine来获取数据与逻辑处理,即一个请求Request,会在多个Goroutine中处理。而这些Goroutine可能需要共享Request的一些信息;同时当Request被取消或者超时的时候,所有从这个Request创建的所有Goroutine也应该被结束。
实现
- context.Context:
是不可变的(immutable)树节点
Cancel 一个节点,会连带 Cancel 其所有子节点 (从上到下)
Context values 是一个节点
Value 查找是回溯树的方式 (从下到上) - context链
package main
func tree() {
ctx1 := context.Background()
ctx2, _ := context.WithCancel(ctx1)
ctx3, _ := context.WithTimeout(ctx2, time.Second * 5)
ctx4, _ := context.WithTimeout(ctx3, time.Second * 3)
ctx5, _ := context.WithTimeout(ctx3, time.Second * 6)
ctx6 := context.WithValue(ctx5, "userID", 12)
}
- context.Context API
3个函数用于限定什么时候你的子节点退出;
1个函数用于设置请求范畴的变量
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Deadline会返回一个超时时间,Goroutine获得了超时时间后,例如可以对某些io操作设定超时时间。
Done方法返回一个信道(channel),当Context被撤销或过期时,该信道是关闭的,即它是一个表示Context是否已关闭的信号。
当Done信道关闭后,Err方法表明Context被撤的原因。
Value可以让Goroutine共享一些数据,当然获得数据是协程安全的。但使用这些数据的时候要注意同步,比如返回了一个map,而这个map的读写则要加锁。
函数、接口、结构体
创建 Context
- 在 RPC 开始的时候,使用 context.Background()
有些人把在 main() 里记录一个 context.Background(),然后把这个放到服务器的某个变量里,然后请求来了后从这个变量里继承 context。这么做是不对的。直接每个请求,源自自己的 context.Background() 即可。 - 如果你没有 context,却需要调用一个 context 的函数的话,用 context.TODO()
- 如果某步操作需要自己的超时设置的话,给它一个独立的 sub-context(如前面的例子)
集成到API里
- 如果有 Context,将其作为第一个变量。
如
func (d* Dialer) DialContext(ctx context.Context, network, address string) (Conn, error)
request 结构体应该以 Request 结束为生命终止, 当 RPC 请求处理结束后,应该去掉对 Context 变量的引用(Unreference)
- 及时关闭
ctx, cancel := context.WithTimeout(parentCtx, time.Second * 2)
defer cancel()
context.Value API 的万金油(duct tape)
type valueCtx struct {
Context
key, val interface{}
}
func WithValue(parent Context, key, val interface{}) Context {
// ...
return &valueCtx{parent, key, val}
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
WithValue() 实际上就是在 Context 树形结构中,增加一个节点.
约束 key 的空间
type privateCtxType string
var (
reqID = privateCtxType("req-id")
)
func GetRequestID(ctx context.Context) (int, bool) {
id, exists := ctx.Value(reqID).(int)
return id, exists
}
func WithRequestID(ctx context.Context, reqid int) context.Context {
return context.WithValue(ctx, reqID, reqid)
}
这里使用 WithXxx 而不是 SetXxx 也是因为 Context 实际上是 immutable 的,所以不是修改 Context 里某个值,而是产生新的 Context 带某个值。
- Context.Value 是 immutable 的,context.Context 从设计上就是按照 immutable (不可变的)模式设计的,同样,Context.Value 也是 immutable 的。
- Context.Value 与request同生共死。
Request 数据衍生出来,并且随着 Request 的结束而终结 - 注意
对于调试非常方便
将必须的信息放入 Context.Value 中,会让接口定义更加不透明
如果可以尽量明确定义在接口
尽量不要用 Context.Value
参考
机制 https://www.cnblogs.com/zhangboyu/p/7456606.html
https://zhuanlan.zhihu.com/p/68792989
https://www.jianshu.com/p/ca7f0629de77
https://studygolang.com/articles/23247?fr=sidebar