本篇文章将从Golang Context包的一些基本使用场景开始,逐步深入,从源码的角度来介绍一下Context的实现原理。最后会给出一些在使用Context时候的一些建议
初识Context
Context是Go1.7之后才出现的一个标准库,Context诞生的主要目的是为了协调多个Goroutine工作,这些协调工作包括:通信(传递消息)、同步、退出
使用Context实现通知退出 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func main () { ctx, cancel := context.WithCancel(context.Background()) go func (ctx context.Context) { for { select { case <-ctx.Done(): fmt.Println("监控退出,停止了..." ) return default : fmt.Println("goroutine监控中..." ) time.Sleep(2 * time.Second) } } }(ctx) time.Sleep(10 * time.Second) fmt.Println("可以了,通知监控停止" ) cancel() time.Sleep(2 * time.Second) }
使用context实现超时控制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func main () { ctx,cancel := context.WithTimeout(context.Background(), 3 *time.Second) defer cancel() go read(ctx) } func read (ctx context.Context) string { select { case <-ctx.Done(): return "" case a<-dataChannel: return a } }
使用context实现goroutine之间的传值 1 2 3 4 5 6 7 8 9 10 11 12 13 type myContextKey string var key myContextKey = "myKey" func main () { ctx := context.WithValue(context.Background(), key, "我是要传入其他goroutine的值" ) go handle(ctx) } func handle (ctx context.Context) { v := ctx.Value(key) if v != nil { fmt.println () } }
Context原理解析 工作机制
由于各个Goroutine之间并没有显式的联系,对于Go来说每个Goroutine都是平等的,并没有父与子的概念,因此当遇到一些复杂的业务的时候,就难以处理,所以Go推出了Context机制来实现对Goroutine的管理。Context采用树结构来管理,第一个被创建的context为树的根节点,可以根据根节点派生出其他的实现context接口的对象,并将其向下传递至新创建的Goroutine,下游Goroutine可以继续派生并向下传递。就这样由一串实现了Context接口的对象构成的树将许多个Goroutine联系了起来。
Context核心接口与结构体 Context接口 :是context中最为核心的接口,所有的context对象都要实现这个接口
1 2 3 4 5 6 7 8 9 10 type Context interface { Deadline() (deadline time.Time, ok bool ) Done() <-chan struct {} Err() error Value(key interface {}) interface {} }
canceler接口 :是具有通知关闭功能的context需要实现的一个拓展接口
1 2 3 4 5 type canceler interface { cancel(removeFromParent bool , err error ) Done() <-chan struct {} }
emptyCtx :该类型以最小程度实现了Context接口,是一个不具备任何功能,并且也不能被关闭的context,该类型存在的目的是作为context对象树的根节点。在Context包中,Golang已经为我们准备了两个emptyCtx的对象,分别是background与todo,我们可以调用方法Background()与TODO()获取到这两个对象,我们一般使用Background()
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 40 41 type emptyCtx int func (*emptyCtx) Deadline() (deadline time.Time, ok bool ) { return } func (*emptyCtx) Done() <-chan struct {} { return nil } func (*emptyCtx) Err() error { return nil } func (*emptyCtx) Value(key interface {}) interface {} { return nil } func (e *emptyCtx) String() string { switch e { case background: return "context.Background" case todo: return "context.TODO" } return "unknown empty Context" } var ( background = new (emptyCtx) todo = new (emptyCtx) ) func Background () Context { return background } func TODO () Context { return todo }
Context的衍生 1 2 3 4 func WithCancel (parent Context) (ctx Context, cancel CancelFunc)func WithTimeout (parent Context, timeout time.Duration) (Context, CancelFunc)func WithDeadline (parent Context, d time.Time) (Context, CancelFunc)func WithValue (parent Context, key, val interface {}) Context
以上的这四个方法用来从一个父context对象衍生出子context对象,我们可以看到前三个方法除了返回了衍生的context之外,还返回了一个CancelFunc方法,其实前三个方法返回的context对象,都实现了cancler接口,接下来我们来具体看一下返回的这个cancelFunc方法
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 type CancelFunc func () func (c *cancelCtx) cancel(removeFromParent bool , err error ) { if err == nil { panic ("context: internal error: missing cancel error" ) } c.mu.Lock() if c.err != nil { c.mu.Unlock() return } c.err = err if c.done == nil { c.done = closedchan } else { close (c.done) } for child := range c.children { child.cancel(false , err) } c.children = nil c.mu.Unlock() if removeFromParent { removeChild(c.Context, c) } }
各个Contex衍生结构的详细分析 cancelCtx 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 type cancelCtx struct { Context mu sync.Mutex done chan struct {} children map [canceler]struct {} err error } func WithCancel (parent Context) (ctx Context, cancel CancelFunc) { c := newCancelCtx(parent) propagateCancel(parent, &c) return &c, func () { c.cancel(true , Canceled) } } func newCancelCtx (parent Context) cancelCtx { return cancelCtx{Context: parent} }
timerCtx
WithTimeOut()与WithDeadLine()返回的都是timerCtx的实例
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 40 41 42 43 type timerCtx struct { cancelCtx timer *time.Timer deadline time.Time } func WithDeadline (parent Context, d time.Time) (Context, CancelFunc) { if cur, ok := parent.Deadline(); ok && cur.Before(d) { return WithCancel(parent) } c := &timerCtx{ cancelCtx: newCancelCtx(parent), deadline: d, } propagateCancel(parent, c) dur := time.Until(d) if dur <= 0 { c.cancel(true , DeadlineExceeded) return c, func () { c.cancel(false , Canceled) } } c.mu.Lock() defer c.mu.Unlock() if c.err == nil { c.timer = time.AfterFunc(dur, func () { c.cancel(true , DeadlineExceeded) }) } return c, func () { c.cancel(true , Canceled) } } func WithTimeout (parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) }
valueCtx
有关于valueCtx的代码非常的简单,主要就有下面两个核心方法,方法内部具体做了什么一看就懂了,就不再赘述了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 type valueCtx struct { Context key, val interface {} } 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} } func (c *valueCtx) Value(key interface {}) interface {} { if c.key == key { return c.val } return c.Context.Value(key) }
关于使用Context时的一些建议与最佳实践
Context要使用参数传递,不要把它作为一个属性放入结构体中
以Context作为参数的方法,应该把Context作为第一个参数
不要给一个Context参数传递nil,当你不需要Context但这个方法的参数里有Context的时候,就传Context.TODO()
Context的Value必须传递必须的值,不要什么值都用Value来传递。Value应该用来传递一些请求范围值(比如说鉴权生成的用户ID、请求ID、用户IP),而不能是全局的(比如数据库连接)。每当你使用Context的Value的时候,你都应该告诉自己Context.Value要做的事情是通知而不是控制。最后你应该尝试尽量不要去用Value
所有可能阻塞或者长时间的操作都应该是可取消的