介绍
在go服务中,每个请求进来都会在自己的goroutine中处理。请求处理程序经常会启动额外的goroutine来访问数据库或rpc服务。这些goroutine处理请求需要用到一些特殊的,例如终端用户身份,授权令牌,请求截止时间。当一个请求被取消或者超时,所有的goroutines应该快速退出,以便系统可以回收他们正在使用的资源。
所以go 在标准库 context 定义了一个上下文context类型来完成上述功能,该类型贯穿了api的边界和进程之间携带截止时间,取消信号量和其他请求范围内的值。
Context 接口
type Context interface {
//获取当前 context 的截止时间,
Deadline() (deadline time.Time, ok bool)
//获取一个只读的 channel,用于识别当前 channel 是否已经被关闭
Done() <-chan struct{}
//获取当前 context 被关闭的原因(超时或cancel),没有被关闭返回nil
Err() error
//获取当前 context 存储的数据
Value(key interface{}) interface{}
}
Context 类型
官方context类型,主要有四种,分别是
- emptyCtx 空context,实现了context接口,但每个方法返回的都是nil
- cancelCtx 用于取消事件的context
- timerCtx 用于超时通知的context
- valueCtx 用于传递上下文信息
emptyCtx
常用context.Background() 或 context.TODO() 方法返回一个emptyCtx。 两种emptyCtx区别如注释所说,其源码实现是一样的,只不过使用场景不同,context.Background()通常由主函数、初始化和测试使用,是顶级Context;context.TODO()通常用于主协程外的其他协程向下传递,分析工具可识别它在调用栈中传播。 源码如下:
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
}
emptyCtx 实际表示是空context,所以他实现context接口方法返回的都是nil emptyCtx实现context接口:
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 any) any {
return nil
}
cancelCtx
context.WithCancel方法会返回cancelCtx和返回用于取消cancelCtx的函数(CancelFunc)。 源码如下:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
propagateCancel方法
func propagateCancel(parent Context, child canceler) {
done := parent.Done()
if done == nil {
return // parent is never canceled
}
select {
case <-done:
// parent is already canceled
child.cancel(false, parent.Err())
return
default:
}
if p, ok := parentCancelCtx(parent); ok {
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 {
atomic.AddInt32(&goroutines, +1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
propagateCancel方法
- 首先会调用parent.Done(),如果为nil 则说明parent是 emptyCtx从无cancel,直接return
- 否则会判断是否parent已经cancel ,若是则会调用当前ctx进行cancel并return
- parentCancelCtx方法找到父类是cancelCtx的,并将当前context加入到父类的children中,用于父类取消事件时一同被通知
- 如果parentCancelCtx方法未被找到则会启动一个新goroutine去监听parent或child的取消事件通知
cancelCtx
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done atomic.Value // of 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
}
cancelCtx结构体属性字段
mu用于并发控制done作为取消信息children用于记录该conetxt对应的所有cancelCtx子集err当被cancel时写入错误信息
cancelFunc函数
// 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
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
} else {
close(d)
}
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}
从代码中看出cancel主要作用就是将cancelCtx 的done属性字段进行赋值closedchan或关闭,err属性字段赋错误信息,然后遍历其子集逐一cancel最后删除子集。
其他coontext接口函数实现如下:
func (c *cancelCtx) Value(key any) any {
if key == &cancelCtxKey {
return c
}
return value(c.Context, key)
}
func (c *cancelCtx) Done() <-chan struct{} {
d := c.done.Load()
if d != nil {
return d.(chan struct{})
}
c.mu.Lock()
defer c.mu.Unlock()
d = c.done.Load()
if d == nil {
d = make(chan struct{})
c.done.Store(d)
}
return d.(chan struct{})
}
func (c *cancelCtx) Err() error {`
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
示例:
func main() {
ctxParent, cancel := context.WithCancel(context.Background())
ctxChild, _ := context.WithCancel(ctxParent)
// parent ctx cancel
cancel()
go func() {
select {
case <-ctxChild.Done():
fmt.Println( "child ctx cancel")
}
}()
select {
case <-ctxParent.Done():
fmt.Println("parent ctx cancel")
}
time.Sleep(time.Second)
}
//output:
//parent ctx cancel
//child ctx cancel
timerCtx
由context.WithTimeout函数返回,其主要作用是timeout和deadline事件,源码如下:
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
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{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 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.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
WithDeadline方法会首先判断当前时间已经过了截止日期,如果过了返回的是一个cancelCtx。 否则会创建一个timerCtx,timerCtx携带一个timer和一个deadline。 它嵌入了一个cancelCtx 到来实现 context接口方法。timerCtx通过time.AfterFunc定时调用 cancel 方法。
valueCtx
由context.WithValue 方法返回,其作用是向上下文传递key-value。 源码如下:
type any = interface{}
type valueCtx struct {
Context
key, val any
}
func WithValue(parent Context, key, val any) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
从结构体发现valueCtx其实就是携带了一个key和value字段,那么valueCtx是如何通过key查询到value呢?
Value方法源码:
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
valueCtx 类型会先查找自身一层的val是否存在,没有则会从父类开始向上逐层查询。
总结
context经常作为一个方法的第一参数,以便向整个链路传递上下文信息如超时时间、特殊值和取消通知。 在context使用过程中并发场景下使用cancelCtx或timerCtx时要考虑是否会出现其中一个goroutine被cancel后其他goroutine也要一起cancel。如果不需要应该为每个goroutine重新建立单独的cancelCtx或timerCtx,以免影响其他goroutine。
参考: