“ 一个接口,四种实现,六个函数。”
01
—
一个接口
golang的context包定义了Context类型,根据官方文档的说法,该类型被设计用来在API边界之间以及过程之间传递截止时间、取消信号及其他与请求相关的数据。Context实际上是一个接口,提供了4个方法:
type Context interface { Deadline() (deadline time.Time, ok bool) Done() struct{} Err() error Value(key interface{}) interface{}}
02
—
四种实现
golang对Context接口的具体实现基于如下几种类型:
type emptyCtx inttype cancelCtx struct { Context mu sync.Mutex // 保护其他字段 done chan struct{} // 延迟创建,被第一个cancel close children map[canceler]struct{} // 被第一个cancel设为nil err error // 被第一个cancel赋值(非nil)}type timerCtx struct { cancelCtx timer *time.Timer // 被cancelCtx.mu保护(并发保护) deadline time.Time}type valueCtx struct { Context key, val interface{}}
03
—
六个函数
为了方便我们使用Context,context包还实现了一组函数:
func Background() Contextfunc TODO() Contextfunc 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 interface{}) Context
01 Background
var ( background = new(emptyCtx) todo = new(emptyCtx))func Background() Context { return background}
ctx := context.Background()
02 TODO
func TODO() Context { return todo}
03 WithCancel
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func main() { var wg sync.WaitGroup ctx := context.Background() ctx1, cancel = context.WithCancel(ctx) wg.Add(1) go func() { defer wg.Done() tick := time.NewTicker(300 * time.Millisecond) for { select { case fmt.Println(ctx1.Err()) return case t := fmt.Println(t.Nanosecond()) } } }() time.Sleep(time.Second) cancel() wg.Wait()}
通过`ctx1.Done()`获取一个channel,监听这个channel可以获得`ctx1`的Cancel通知,还可以通过
`ctx.Err()`
获取
`ctx1`
被取消时写入的错误信息。
04 WithDeadline
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func main() { var wg sync.WaitGroup ctx := context.Background() ctx2, cancel := context.WithDeadline(ctx,time.Now().Add(time.Second)) defer cancel() wg.Add(1) go func() { defer wg.Done() tick := time.NewTicker(300 * time.Millisecond) for { select { case fmt.Println(ctx2.Err()) return case t := fmt.Println(t.Nanosecond()) } } }() wg.Wait()}
如果WithDeadline函数接收到的是一个有截止时间的Context,那就要比较一下这两个截止时间了。例如下面再基于
ctx2
创建一个Context:
ctx3, cancel := context.WithDeadline(ctx2, time.Now().Add(time.Second*2))
如果
ctx3
要设定的截止时间比
ctx2
还晚,那么就没必要给它加定时器和截止时间了。只会用WithCancel函数基于
ctx2
构造一个cancelCtx,所以
ctx3
的动态类型是
*cancelCtx
。
上面的
ctx3
是基于
ctx2
创建的,而
ctx2
又是基于
ctx
创建的。这样层层包装的结果就是,这些Context会形成树状结构,子节点是基于父节点的包装,每个节点都可能有零个或多个子节点。
对于可取消的Context而言,也就是实现了
context.canceler
接口的类型,还有一个关键点,就是它们会被注册到距离当前Context节点最近的、可取消的祖先节点中。注册位置就在cancelCtx结构体的
children
map中。而
*cancelCtx和
*timerCtx都实现了
canceler
接口,所以
ctx3
会被注册到
ctx2
中。
这样如果
ctx2
先取消,就可以根据
children
map这里的记录,取消自己子节点中所有可Cancel的Context。
05 WithTimeout
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout))}
06 WithValue
func WithValue(parent Context, key, val interface{}) Context { if key == nil { panic("nil key") } if !reflect.TypeOf(key).Comparable() { panic("key is not comparable") } return &valueCtx{parent, key, val}}
`WithValue`将传入的`parent` Context和`key`、`val`打包成一个新的`ctx`,再借助`valueCtx`的`Value`方法就可以通过Context传递数据了。使用Context传递数据要特别注意:
- Context是本着不可改变(immutable)的模式设计的,所以
不要试图修改Context里保存的数据
。
- 为了避免后续包装的键值对覆盖先前的值,最好不要直接使用
string
、
int
这类基础类型,而是用自定义类型包装一下。
下面我们来测试一下上述第二点:
package mainimport ( "context" "fmt")var keyA string = "keyA"var keyC string = "keyA"func main() { ctx := context.Background() ctx1 := context.WithValue(ctx, keyA, "valueA") fmt.Println("In ctx:") fmt.Println("keyA => ", ctx1.Value(keyA)) ctx2 := context.WithValue(ctx1, keyC, "valueC") fmt.Println("In ctx2:") fmt.Println("keyA => ", ctx2.Value(keyA)) fmt.Println("keyC => ", ctx2.Value(keyC)) return}
In ctx:keyA => valueAIn ctx2:keyA => valueCkeyC => valueC
再结合valueCtx.Value方法看一下键值对查找过程:
func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { return c.val } return c.Context.Value(key)}
type akeytype stringtype ckeytype stringvar keyA akeytype = "keyA"var keyC ckeytype = "keyA"......
In ctx:keyA => valueAIn ctx2:keyA => valueAkeyC => valueC
WithCancel
、
WithDeadline
和
WithValue
函数,内部就是分别构造了
cancelCtx
、
timerCtx
和
valueCtx
这3种结构,最终所有的
ctx
将构成类似一棵树的结构,在根节点执行cancel就可以Cancel整个棵树,所以非常适合用来控制请求处理。
Go语言的http和sql包中都有Context的应用,这些待到“应用篇”再接着聊~
戳这里了解接口~
点个赞呗