Golang中的context有什么用?
context是golang标准库中的一个包,它定义了一个上下文类型,这个上下文类型可以在线程间,携带信号、值、超时时间等,起到识别和跟踪go中的每个goroutine的作用,进而达到控制它们的目的。比如我们可以对某个goroutine设置一个超时时间,然后就可以实现对该goroutine下所有衍生的子goroutine达到级联取消的作用。
context的底层数据结构
context首先是被定义成了一个接口,这个接口包含了4个方法:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
复制代码
- Deadline() 可以获取到当前context是否被设置了过期时间,如果设置了,可以获取到它被设置的过期时间是什么
- Done() 返回一个只可以读的chan, 当当前的context被取消或者到了Deadline,则会返回一个被关闭的channel
- Err() 在Done() 被关闭后,返回一个context被关闭的原因
- Value(key interface{}) 可以获取到key对应的value
另外context包中还有一个canceler接口:
// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
复制代码
实现了canceler的context是可以被直接取消的,然后在context包中,有emptyCtx,cancelCtx和timerCtx都对这个接口进行了实现。
虽然emptyCtx对context接口进行了实现,但是它是一个空实现,在我们实际使用的时候,相当于是一个root的context
接着是cancelCtx的结构体:
342 // A cancelCtx can be canceled. When canceled, it also cancels any children
343 // that implement canceler.
344 type cancelCtx struct {
345 Context
346
347 mu sync.Mutex // protects following fields
348 done chan struct{} // created lazily, closed by first cancel call
349 children map[canceler]struct{} // set to nil by the first cancel call
350 err error // set to non-nil by the first cancel call
351 }
复制代码
cancelCtx组合了Context,并且它还实现了canceler接口,说明这是一个可以被取消的context。
接着我们看下propagateCancel的具体实现:
232 func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
233 if parent == nil {
234 panic("cannot create context from nil parent")
235 }
236 c := newCancelCtx(parent)
237 propagateCancel(parent, &c)
238 return &c, func() { c.cancel(true, Canceled) }
239 }
……
……
249 // propagateCancel arranges for child to be canceled when parent is.
250 func propagateCancel(parent Context, child canceler) {
251 done := parent.Done()
//根的context永远不会被取消, 这个根context一般是基于context.Backgroud()或者是context.Todo()生成出来的
252 if done == nil {
253 return // parent is never canceled
254 }
255
// 这段select好像旧的版本里面是没有的, 当发现父context已经被取消了, 则直接调用cancel取消父context下的所有子context
256 select {
257 case <-done:
258 // parent is already canceled
259 child.cancel(false, parent.Err())
260 return
// 注意这个default, 当select中的case没有收到信号的时候, 会直接执行default, 所以这个select并不会阻塞整个方法的执行
261 default:
262 }
263
// 查找父context的*cancelCtx
264 if p, ok := parentCancelCtx(parent); ok {
265 p.mu.Lock()
// 找到了并且已经取消的话, 则取消所有的子context
266 if p.err != nil {
267 // parent has already been canceled
268 child.cancel(false, p.err)
269 } else {
// 初始化一个子context的map, 用于挂载子的context
270 if p.children == nil {
271 p.children = make(map[canceler]struct{})
272 }
273 p.children[child] = struct{}{}
274 }
275 p.mu.Unlock()
276 } else { // 如果没有找到
277 atomic.AddInt32(&goroutines, +1)
// 启动一个goroutine监听父context的取消信号
278 go func() {
279 select {
// 如果父context取消, 则级联取消它下面挂的所有子的context
280 case <-parent.Done():
281 child.cancel(false, parent.Err())
// 这个case的主要的目的是:
// 假如父的context一直不取消的话,那么当子的context取消后,
// 这个监听的goroutine没有存在的必要,相对于这个goroutine泄露了,除非父的context取消。
// 为什么没有存在的必要呢?
// 因为子的context已经取消,跟父的context也不存在任何关系了,
// 父context取消,会调用cancel去取消所有的子context,
// 没有必要单独起保留这个goroutine对信号进行监听
282 case <-child.Done():
283 }
284 }()
285 }
286 }
复制代码
propagateCancel的作用是为生成的子context安排一个可以挂靠的父context,这样当某个父context取消的时候,才能实现级联式的取消它下面所有的子context。具体的说明写在代码注释里面了。
然后是cancel的实现:
392 // cancel closes c.done, cancels each of c's children, and, if
393 // removeFromParent is true, removes c from its parent's children.
394 func (c *cancelCtx) cancel(removeFromParent bool, err error) {
395 if err == nil {
396 panic("context: internal error: missing cancel error")
397 }
398 c.mu.Lock()
399 if c.err != nil {
400 c.mu.Unlock()
401 return // already canceled
402 }
403 c.err = err
404 if c.done == nil {
405 c.done = closedchan
406 } else {
407 close(c.done)
408 }
409 for child := range c.children {
410 // NOTE: acquiring the child's lock while holding parent's lock.
411 child.cancel(false, err)
412 }
413 c.children = nil
414 c.mu.Unlock()
415
416 if removeFromParent {
417 removeChild(c.Context, c)
418 }
419 }
复制代码
其中最重要的就是removeFromParent
参数,我们看在WithCancel
方法中调用cancel时候传的是true
,其他时候传都是false
。这个参数控制着WithCancel
是否被调用,所以我们可以看下WithCancel
具体做了哪些事情。
315 // removeChild removes a context from its parent.
316 func removeChild(parent Context, child canceler) {
317 p, ok := parentCancelCtx(parent)
318 if !ok {
319 return
320 }
321 p.mu.Lock()
322 if p.children != nil {
323 delete(p.children, child)
324 }
325 p.mu.Unlock()
326 }
复制代码
可以看到,这个方案其实就是从父的context中所有的子context移除了自己。那这个removeFromParent
想要说明的是什么呢?其实我的理解就是:如果我们取消的是当前context,那么我们理所当然的应该将当前的context从它的父context的children集合中移除,但是我们移除当前context的子context的时候就不需要这么做了,因为我们在移除当前context的子context的时候,直接让context.children = nil就可以了
。
timerCtx的结构体的实现其实也是基于cancelCtx, 只不过它是新增了一个timer,通过定时器定时的去cancel,进而实现goroutine的定时取消。
context超时控制使用的基本姿势
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
s, err := GetData(ctx)
if err != nil {
fmt.Printf("error: %+v, s: %s \n", err, s)
return
}
fmt.Printf("s: %+v \n", s)
}
func GetData(ctx context.Context) (string, error) {
c := make(chan error)
s := ""
go func() {
time.Sleep(1 * time.Second)
s = "test"
c <- nil
}()
select {
case <-ctx.Done():
return "error", ctx.Err()
case <-c:
return s, nil
}
}
复制代码
如上, 在GetData方法中,我们去获取数据的时候,可以启动了一个goroutine去获取数据,比如发起一个HTTP的请求什么的,当超过2s没有数据返回的话呢,select中的ctx.Done() 就会受到一个信号,从而返回一个错误。在上面我的go func里面只是sleep了1s的时间,在实际的项目中,我们还需要注意,这个goroutine是否会泄露的问题,比如,如果超时了之后,GetData我们是直接返回了错误,但是这个go func这个goroutine什么时候退出我们一定要自己心里有数。