1介绍及使用场景
相信大家都经历过面试官的灵魂拷问:
context 的作用是啥??
父context取消后,子context会立即取消吗?
今天让我们深入了解context,然后继续被面试官虐(* ̄︶ ̄)
context 是Go 1.7 引入标准库,被称作上下文,通常在goroutine间传递上下文,作用包括取消信号、超时控制、k-v 传递。k-v 传递通常用来做分布式链路追踪,上下文传值。
在一些需要超时控制的场景我们经常会看到它的身影,比如sql 里面,像ping 这样的场景,ping 什么时候结束?不结束肯定会一直等待着,那遇到异常场景就会一直阻塞了,显然是不行的。像http 请求也是需要超时控制,不控制,就会产生goroutine 泄露,不知道请求什么时候结束,那么请求就会一直等着。
// PingContext verifies a connection to the database is still alive,
// establishing a connection if necessary.
func (db *DB) PingContext(ctx context.Context) error {
var dc *driverConn
var err error
for i := 0; i < maxBadConnRetries; i++ {
dc, err = db.conn(ctx, cachedOrNewConn)
if err != driver.ErrBadConn {
break
}
}
if err == driver.ErrBadConn {
dc, err = db.conn(ctx, alwaysNewConn)
}
if err != nil {
return err
}
return db.pingDC(ctx, dc, dc.releaseConn)
}
2 源码解析
结构
context 接口
context 携带截止日期,超时取消信号,和其他value,通过下面这些接口,下面这些接口方法会同时被多个goroutines 调用
// A Context carries a deadline, a cancellation signal, and other values across
// API boundaries.
//
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
// Deadline returns the time when work done on behalf of this context
// should be canceled. Deadline returns ok==false when no deadline is
// set. Successive calls to Deadline return the same results.
Deadline() (deadline time.Time, ok bool)
// Done returns a channel that's closed when work done on behalf of this
// context should be canceled. Done may return nil if this context can
// never be canceled. Successive calls to Done return the same value.
// The close of the Done channel may happen asynchronously,
// after the cancel function returns.
//
// WithCancel arranges for Done to be closed when cancel is called;
// WithDeadline arranges for Done to be closed when the deadline
// expires; WithTimeout arranges for Done to be closed when the timeout
// elapses.
//
// Done is provided for use in select statements:
//
// // Stream generates values with DoSomething and sends them to out
// // until DoSomething returns an error or ctx.Done is closed.
// func Stream(ctx context.Context, out chan<- Value) error {
// for {
// v, err := DoSomething(ctx)
// if err != nil {
// return err
// }
// select {
// case <-ctx.Done():
// return ctx.Err()
// case out <- v:
// }
// }
// }
//
// See https://blog.golang.org/pipelines for more examples of how to use
// a Done channel for cancellation.
Done() <-chan struct{}
// If Done is not yet closed, Err returns nil.
// If Done is closed, Err returns a non-nil error explaining why:
// Canceled if the context was canceled
// or DeadlineExceeded if the context's deadline passed.
// After Err returns a non-nil error, successive calls to Err return the same error.
Err() error
// Value returns the value associated with this context for key, or nil
// if no value is associated with key. Successive calls to Value with
// the same key returns the same result.
//
// Use context values only for request-scoped data that transits
// processes and API boundaries, not for passing optional parameters to
// functions.
//
// A key identifies a specific value in a Context. Functions that wish
// to store values in Context typically allocate a key in a global
// variable then use that key as the argument to context.WithValue and
// Context.Value. A key can be any type that supports equality;
// packages should define keys as an unexported type to avoid
// collisions.
//
// Packages that define a Context key should provide type-safe accessors
// for the values stored using that key:
//
// // Package user defines a User type that's stored in Contexts.
// package user
//
// import "context"
//
// // User is the type of value stored in the Contexts.
// type User struct {...}
//
// // key is an unexported type for keys defined in this package.
// // This prevents collisions with keys defined in other packages.
// type key int
//
// // userKey is the key for user.User values in Contexts. It is
// // unexported; clients use user.NewContext and user.FromContext
// // instead of using this key directly.
// var userKey key
//
// // NewContext returns a new Context that carries value u.
// func NewContext(ctx context.Context, u *User) context.Context {
// return context.WithValue(ctx, userKey, u)
// }
//
// // FromContext returns the User value stored in ctx, if any.
// func FromContext(ctx context.Context) (*User, bool) {
// u, ok := ctx.Value(userKey).(*User)
// return u, ok
// }
Value(key interface{}) interface{}
}
Deadline()
返回任务被取消时间,返回ok=false,当Deadline 没有设置的时候,比如下面的emptyCtx,除了timeCtx,其他接口都会选择继承父context 的 Deadline,当是emptyCtx 为nil,timeCtx为到期时间。
Done()
-
返回一个channel,当任务被取消时会被关闭
-
在cancel 方法返回时,channel 关闭可能是异步的
-
WithCancel 的作用是安排在调用cancel 时关闭Done()
-
WithDeadline的作用是安排在到达deadline 截止时期关闭Done()
-
Done() 通常配合select使用,如下面控制超时的例子
func Stream(ctx context.Context, out chan<- Value) error {
for {
v, err := DoSomething(ctx)
if err != nil {
return err
}
select {
case <-ctx.Done(): //监控ctx.done
return ctx.Err()
case out <- v: //将值投入到out channel,如果channel 发送阻塞不能投递,就直接返回了
}
}
}
Err() error
-
done 没有关闭,那么Err获取到的一定是nil
-
done 被关闭了,Err 返回的error 解释了为什么context 被取消了或者context's deadline 超时
-
连续调用Err(),会返回同样的error
-
Value(key interface{}) interface{} 根据key 获取值,相当于一个key value 的存储,返回类型是接口类型
emptyCtx
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
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
}
实现了context接口,所有返回值都是空,有下面两个常用实现,这个context都不能被取消,也不能存储值,可以理解为最原始的根context
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
return background
}
// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
return todo
}
cancelCtx
// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done chan struct{} // created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
func (c *cancelCtx) Value(key interface{}) interface{} {
if key == &cancelCtxKey {
return c
}
return c.Context.Value(key)
}
//这里done 是赖生成的,如果调用Done,c.done 将不会nil
func (c *cancelCtx) Done() <-chan struct{} {
c.mu.Lock()
if c.done == nil {
c.done = make(chan struct{})
}
d := c.done
c.mu.Unlock()
return d
}
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
-
value方法将会在遇到cancelCtxKey将会返回自己
timerCtx
timerCtx携带了一个timer和自身携带deadline,继承了cancelCtx的Done 和 Err,实现了cancel 方法,在停止timer 的时候,调用cancelCtx.cancel取消
// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(false, err)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
valueCtx
// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
Context
key, val interface{}
}
//
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
-
valueCtx 只实现了Value 方法,其他方法继承父Context,先从自己的key 进行查找,如果没找到,调用父Context的Value 方法继续往上找,整个过程相当于链表的遍历,直到找到值,或者遇到empty context返回nil
使用
WithCancel
// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent) //创建一个CancelCtx
propagateCancel(parent, &c)//繁殖Cancel,将parent 和child(c)进行关联
return &c, func() { c.cancel(true, Canceled) }
}
-
WithCancel 返回parent 的拷贝,和一个cancel函数,当调用这个cancel,便会级联取消
newCancelCtx
返回初始化的cancelCtx,就是将cancelCtx的context 赋值为parent
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
propagateCancel
该函数作用就是将child 挂靠在parent 的children 集合上面,让parent 取消,child 也能取消,parent 必须是cancelctx ,因为只有挂靠在cancelctx才能达到级联取消效果
// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
done := parent.Done() //获取 parent.done
if done == nil {//如果为nil,直接返回就行
return // parent is never canceled
}
select {
case <-done: //如果parent 已经被cancel 直接cancel child
// parent is already canceled
child.cancel(false, parent.Err())
return
default: //如果parent 没有被取消,则直接走下面默认逻辑
}
if p, ok := parentCancelCtx(parent); ok { //寻找最近的cancelctx
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else { //来到这里的话说明是自定义cancelctx或者没有找到cancelctx
atomic.AddInt32(&goroutines, +1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
-
先判断done == nil,如果成立直接返回,说明这时候为nil,查看上面结构,发现只有emptyctx done 返回nil,cancleCtx会返回make 后的的done,其他两个ctx 未实现继承传入的done()方法。
如果来到这里说明,参数是context.TODO或者context.Background
-
如果parent 已经被取消直接取消child 返回就行了
-
parentCancelCtx 从 父节点找到最近的一个cancleCtx,如果成功了的话走下面逻辑
1、p.err != nil 代表cancleCtx被取消过了,所以应该马上取消child
2、p.err = nil 将child放进cancleCtx的children集合里面
-
else 分支说明是自定义cancelctx,这时候为什么还要监听parent.Done()呢?
parentCancelCtx返回false 有三种情况,一种是emptyctx或者该Context 被赋值为closedchan
第二种是没有找到cancelCtxKey这种情况,第三种是done 不匹配,cancelCtx被继承了,但是返回了不一样的done
-
下面最后一句go func()的作用:
1、假如因为 parentCancelCtx中 done == closedchan || done == nil 返回,说明这个CancelCtx要么被取消了,要么父context没有CancelCtx,此时parent.Done()直接返回了,然后再取消child就行了。
2、 p.done != done,当找到的父CancelCtx是自定义的时候,我们也要监听,因为没有信号通知child 取消
,所以当自定义的CancelCtx 取消的时候,也要监听这个,取消child。
为什么 还要 <- child.Done() ?假设child 取消了,parent 没有取消了,那这个协程就会一直阻塞在这,propagateCancel的作用就是关联父context 与child。所以在子child 取消的时候,我们应该直接返回就行了。
parentCancelCtx
// parentCancelCtx returns the underlying *cancelCtx for parent.
// It does this by looking up parent.Value(&cancelCtxKey) to find
// the innermost enclosing *cancelCtx and then checking whether
// parent.Done() matches that *cancelCtx. (If not, the *cancelCtx
// has been wrapped in a custom implementation providing a
// different done channel, in which case we should not bypass it.)
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
done := parent.Done()
if done == closedchan || done == nil {
return nil, false
}
p, ok := parent.Value(&cancelCtxKey).(*cancelCtx) //从parent查找cancelCtx
if !ok {
return nil, false
}
p.mu.Lock()
ok = p.done == done //判断这两个done 是不是一样
p.mu.Unlock()
if !ok {
return nil, false
}
return p, true
}
-
如果parent.done 被关闭了或者是nil,直接返回false
什么时候done == closedchan?
答:在cancelCtx.cancel+464 行,如果c.done等于nil,c.done 会被赋值为closedchan
什么时候done == nil?
答:parent.Done()只有一种清空为nil,那就是context.TODO或者context.Background,因为cancelctx 在调用Done()时会make,不为nil,其他两种情况的ctx都是继承,没有自己实现done。
-
从parent 里面查找cancelCtxKey所对应的cancelCtx,还记得上面cancelCtx Value方法在实现时,遇到cancelCtxKey会返回自己
-
需要对返回的done 做个比较,判断p.done 是不是done ,如果不是这这个context是自定义包装过的,并且返回的是不同的done channel
cancel
// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
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 // already canceled
}
c.err = err
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}
for child := range c.children { //循环遍历children 调用child.cancel
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
if removeFromParent { //如果removeFromParent ,将child 从parent(c.Context)里面移除
removeChild(c.Context, c)
}
}
-
什么时候c.done == nil?
cancelCtx.done不为nil只有一种情况,调用cancelCtx.Done(),如果像下面这种情况,没有人调用ctx.Done(),c.done 就为nil被赋值为closedchan
package main
import "context"
func main() {
_,cancel:=context.WithCancel(context.Background())
//go func() {
// select {
// case <-ctx.Done():
//
// }
//}()
cancel()
}
-
cancel 关闭c.done,还记得我们调用case<-ctx.Done()这种情况,一旦c.done 被关闭,那么case就会被调用,到达退出协程的目的
-
递归调用cancelCtx.children的cancel取消函数
cancel 流程
如果所示,举个例子来说明整个流程
情况1:A 被调用cancel或者A已经被调用cancel,这时候跟B的流程一样,这时候是找不到父cancelCtx ,在最后调用CancelFunc时,removeChild 直接返回了
情况2:B 被调用cancel,这时候,循环遍历B 的children,分别调用C,D的cancel,最后将自己的children 置为nil,最后根据removeFromParent选择是否从A 的children移除,在最后调用CancelFunc时,removeChild 就会从A 的children移除,所以注意不要忘记调用CancelFunc
情况3:D或C 被cancel,跟B 的流程一样,最后会从B 的children移除
removeChild
该函数作用是将context从父context移除,父context也必须是 cancelctx ,来看看我们前面的removeFromParent,当Withxx 返回CancelFunc调用时值是true,这时候调用context.cancel 会将自己从父context 移除,而当propagateCancel 时调用context.cancel 和遍历children 调用cancel 都传的false,这是因为这里不需要removeChild.
-
在调用context.cancel时,会将该cancelctx的children 直接置空,所以不需要在去一个一个将该child从该cancelctx的children移除
-
到最后调用CancelFunc的时候,需要直接将child 从父cancelctx的children 里面移除
// removeChild removes a context from its parent.
func removeChild(parent Context, child canceler) {
p, ok := parentCancelCtx(parent) //找到最近一个cancelCtx
if !ok { //没找到,直接返回
return
}
p.mu.Lock() //加锁操作
if p.children != nil {//如果cancelctx.children不为nil,将child 从parent 里面删除
delete(p.children, child)
}
p.mu.Unlock()
}
WithDeadline
// WithDeadline returns a copy of the parent context with the deadline adjusted
// to be no later than d. If the parent's deadline is already earlier than d,
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
// context's Done channel is closed when the deadline expires, when the returned
// cancel function is called, or when the parent context's Done channel is
// closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{ //创建timerCtx
cancelCtx: newCancelCtx(parent),
deadline: d,
}
propagateCancel(parent, c) //调用这个函数处理parent 和child 的关系
dur := time.Until(d) //判断当前距离截止时间还有多少时间
if dur <= 0 {//时间小于0,说明已经过期了,超过截止时间了,该调用取消函数
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(false, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil { //如果c.err 为nil,说明没有取消,则添加定时器到点取消
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
timerCtx. cancel
func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(false, err) //调用cancelCtx.cancel
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop() //停止定时器,释放资源
c.timer = nil
}
c.mu.Unlock()
}
WithTimeout
// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete:
//
// func slowOperationWithTimeout(ctx context.Context) (Result, error) {
// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
// defer cancel() // releases resources if slowOperation completes before timeout elapses
// return slowOperation(ctx)
// }
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
-
就是在WithDeadline基础加上了timeout,传进来的timeout是超时时间,代表距现在多少时间后超时,
所有将现在时间+timeout就是什么时间点过期,直接调用WithDeadline就可以了
-
调用CancelFunc 释放资源,所以在操作完成后,我们应该调用cancel 函数,通常使用defer
WithValue
// WithValue returns a copy of parent in which the value associated with key is
// val.
//
// Use context Values only for request-scoped data that transits processes and
// APIs, not for passing optional parameters to functions.
//
// The provided key must be comparable and should not be of type
// string or any other built-in type to avoid collisions between
// packages using context. Users of WithValue should define their own
// types for keys. To avoid allocating when assigning to an
// interface{}, context keys often have concrete type
// struct{}. Alternatively, exported context key variables' static
// type should be a pointer or interface.
func WithValue(parent Context, key, val interface{}) Context {
if parent == nil { //parent 为空报错
panic("cannot create context from nil parent")
}
if key == nil {//key 为空报错
panic("nil key")
}
//通过反射key 判断该key 是否可比较,不可比较报错
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
//返回value context
return &valueCtx{parent, key, val}
}
-
WithValue通过key 和value 返回parent 的拷贝,很好理解,因为创建了新的一个context继承parent,但是多了一个value
面试八股文
最后再来一个面试八股文
context 应用注意事项?
来自于go官方博客里,对的建议:
-
Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx.
-
Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use.
-
Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.
-
The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines.
中文译文
-
不要将 Context 作为结构体字段。应该将 Context 类型作为函数的第一参数,并且通常命名为 ctx。看到过知名项目这样做过(* ̄︶ ̄)
-
不要向函数传入一个 nil 的 context,不知道传什么就todo
-
不应该将可选的函数参数进到 context ,context 应该存储的是一些有请求范围有共性的数据。例如: session、cookie ,traceId等。
-
同一个 context 在多个 goroutine传递是并发安全的,因为有锁