深入解释Go中context使用方式


这篇文章详细介绍了Go 语言中context 函数背后的实现细节和代码,帮助开发人员了解上下文包的底层工作原理。


我们来看一个使用 context 包的简单示例:该函数接受一个上下文并将其传递给另一个函数,因为对于大多数人来说,这就是上下文的全部,只是在函数需要时传递的东西。

func main() {
    bigFunc(context.Background())
}

func bigFunc(ctx context.Context) {
    smallFunc(ctx)
}

func smallFunc(ctx context.Context) {
    // I don't know what to do with it, let' just print it
    fmt.Println(ctx)
}

将打印出 context.Background。这是因为 context.Background 返回的内容满足 Stringer 接口的要求,而 Stringer 接口在调用 String.Background 时只会返回该内容。

context接口
让我们从最基本的开始。我们使用的 context.Context 类型是一个接口,下面是它的定义。

type Context interface {
    Deadline() (deadline time.Time, ok bool) // 赶时间
    Done() <-chan struct{}                  
// 获取取消时关闭的通道
    Err() error                              
// 如果 Done 通道已关闭,则返回非零
    Value(key any) any                      
// 从上下文存储中获取值
}

任何满足此接口的结构都是有效的上下文对象。如果注释中没有说明,让我们快速了解一下它们分别是什么。

  • Deadline:该函数返回设置为截止日期的时间,例如,如果上下文是使用 context.WithDeadline 创建的。
  • Done:该函数返回一个在取消上下文时关闭的通道。
  • Err:如果取消已发生,则返回非零。
  • Value:值:该函数用于获取存储在上下文实例中的值。

如果你想创建一个 "Context",这些就是你所需要的,而且你可以很容易地创建它们。尽管如此,stdlib 还是为我们提供了一些有用的 "上下文"。

emptyCtx 结构
这是一个结构体,满足成为 Context 的最基本要求。代码如下

type emptyCtx struct{}

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
}

如您所见,它什么也不做,但这就是 context.Background 和 context.TODO 中的主要内容。

context.Background 和 context.TODO
这两种方法都只是 emptyCtx 加上一个 String 方法,以满足 Stringer 接口的要求。它们提供了一种创建空基础上下文的方法。它们之间唯一的区别就是名称不同。

  • 当你知道需要一个空上下文时,比如在刚刚开始运行的 main 中,你可以使用 context.Background;
  • 当你不知道使用什么上下文或还没有接好线时,你可以使用 context.TODO。

您可以将 context.TODO 视为类似于在代码中添加 // TODO 注释。

 context.Background:

type backgroundCtx struct{ emptyCtx }

func (backgroundCtx) String() string {
    return "context.Background"
}

func Background() Context {
    return backgroundCtx{}
}

 context.TODO:

type todoCtx struct{ emptyCtx }

func (todoCtx) String() string {
    return "context.TODO"
}

func TODO() Context {
    return todoCtx{}
}


context.WithValue
现在,我们将进入 context 软件包的更多实用案例。如果想使用 context 传递一个值,可以使用 context.WithValue。您可能见过日志或网络框架使用这种方法。

让我们看看它的内部结构:

type valueCtx struct {
    Context
    key, val any
}

func WithValue(parent Context, key, val any) Context {
    return &valueCtx{parent, key, val}
}

它只是返回一个包含父上下文、键和值的结构体。

如果你注意到,该实例只能包含一个键和一个值,但你可能在网络框架中看到过,它们会从 ctx 参数中提取多个值。由于我们将父上下文嵌入到了较新的上下文中,因此可以向上递归搜索以获取其他任何值。

bgCtx := context.Background()
v1Ctx := context.WithValue(bgCtx, "one", "uno")
v2Ctx := context.WithValue(v1Ctx,
"two", "dos")

现在,如果我们要从 v2Ctx 中获取 "one "的值,可以调用 v2Ctx.Value("one")。
这将首先检查 v2Ctx 中的键是否为 "one",
如果不是,则将检查父节点(v1Ctx)的键是否为 "one"。
既然 v1Ctx 中的键是 "one",我们就返回上下文中的值。

下面代码递归搜索父上下文,查看其中是否有匹配键的上下文,然后返回其值。

func (c *valueCtx) Value(key any) any {
    // If it this one, just return it
    if c.key == key {
        return c.val
    }
    return value(c.Context, key)
}

func value(c Context, key any) any {
    for {
        switch ctx := c.(type) {
        case *valueCtx:
            
// 如果父节点是`valueCtx`,则检查其键
            if key == ctx.key {
                return ctx.val
            }
            c = ctx.Context
        case backgroundCtx, todoCtx:
            
// 如果我们已到达顶部,即基本上下文
          
// 返回,因为我们什么也没找到。
            return nil
        default:
           如果是其他上下文,
        
//只需调用其`.Value`方法。
            return c.Value(key)
        }
    }
}

context.WithCancel
让我们来看看更有用的东西。您可以使用上下文包创建一个 ctx,用来向下游函数发出取消信号。

让我们来看一个如何使用的示例:

func doWork(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            return
        default:
        }

        time.Sleep(1 * time.Second)
        fmt.Println("doing work...")
    }
}

func main() {
    bgCtx := context.Background()
    innerCtx, cancel := context.WithCancel(bgCtx)

    go doWork(innerCtx)
// call goroutine
    time.Sleep(3 * time.Second)
// do work in main

    
// well, if `doWork` is still not done, just cancel it
    cancel()
}

在这种情况下,您可以通过在主函数中调用 cancel 来向 doWork 函数发出停止工作的信号。

现在来看看它是如何工作的。让我们从函数定义开始(我们很快就会讲到结构定义):

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := &cancelCtx{}
    c.propagateCancel(parent, c)
    return c, func() { c.cancel(true, Canceled, nil) }
}

当您调用 context.WithCancel 时,它会返回一个 cancelCtx 实例和一个函数,您可以调用该函数来取消上下文。由此我们可以推断,cancelCtx 是一个具有取消函数的上下文,该函数可用来 "取消 "上下文。

如果你忘记了,取消上下文只是意味着关闭 Done() 返回的通道。

顺便说一下,在此上下文中,propagateCancel 函数的主要作用是创建一个 cancelCtx,以及在创建之前确保父节点尚未被取消。

好了,现在让我们来看看结构图,之后我们将了解它是如何工作的(source).

type cancelCtx struct {
    Context

    mu       sync.Mutex            // protects following fields
    done     atomic.Value          
// chan struct{} for Done
    children map[canceler]struct{}
    err      error
    cause    error
}

这里:

  • Context:保存父上下文
  • mu sync.Mutex:你知道这是什么吧
  • done atomic.Value:保存将由 Done() 函数返回的 chan struct{}
  • err error:保存导致取消的错误信息
  • cause error:保存取消的原因,即取消函数的最后一个参数

对了,这是取消功能(source):

func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
    if cause == nil {
        cause = err
    }

    c.err = err
    c.cause = cause

    // 从 atomic.Value 中加载 chan struct{} 并关闭它
    d, _ := c.done.Load().(chan struct{})
    if d == nil {
        
//如果不存在,则存储一个封闭通道
        c.done.Store(closedchan)
    } else {
        
// 如果存在,则关闭
        close(d)
    }

    
// 对所有子代调用取消,以传播取消信息
    for child := range c.children {
        child.cancel(false, err, cause)
    }
    c.children = nil

    
// 从父节点移除
    if removeFromParent {
        removeChild(c.Context, c)
    }
}

首先,我们在实例上设置原因和错误,然后关闭 Done 返回的通道。之后,它会取消所有子实例,最后将自己从父实例中移除。

Context.WithDeadline 和context.WithTimeout 
当您想创建一个在到达截止日期时自动取消的上下文时,这些功能就非常有用。这对于执行服务器超时等操作非常有用。

首先,context.WithTimeout 只是计算截止时间并调用 context.WithDeadline。事实上,这就是它的全部代码:

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

现在进入细节部分。有些人可能已经猜到,WithDeadline 基本上就是一个普通的 WithCancel 上下文,只不过是由上下文包来处理取消。

让我们看看代码的作用。这里有函数 WithDeadlineCause 的代码,它是 WithDeadline 的一个变体,但增加了传递取消的 "原因 "的功能。顺便提一下,其他上下文包函数也可以使用 Cause 变体,而像 WithDeadline 这样的非 Cause 变体只是在调用 Cause 变体时将 cause 设为 nil。

func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
    if cur, ok := parent.Deadline(); ok && cur.Before(d) {
        // 如果父节点的截止时间早于子节点,
        
// 只需返回一个 cancelCtx
        return WithCancel(parent)
    }

    
// create a new timerCtx
    c := &timerCtx{
        deadline: d,
    }
    c.cancelCtx.propagateCancel(parent, c)

    dur := time.Until(d)
    if dur <= 0 {
        c.cancel(true, DeadlineExceeded, cause)
// deadline has already passed
        return c, func() { c.cancel(false, Canceled, nil) }
    }

    
// 如果一切正常,则设置新的计时器,并返回取消函数
    if c.err == nil {
        c.timer = time.AfterFunc(dur, func() {
            c.cancel(true, DeadlineExceeded, cause)
        })
    }
    return c, func() { c.cancel(true, Canceled, nil) }
}

  • 如果父节点的截止日期早于子节点,则返回从父节点创建的简单 cancelCtx
  • 如果不是,则创建一个新的 timeCtx(结构定义如下)
  • 现在检查是否已超过截止时间,如果是,则取消已创建的上下文并返回
  • 如果没有,我们将使用 time.AfterFunc 设置一个计时器来执行取消操作,然后返回

timerCtx 只是一个带有计时器的 cancelCtx:

type timerCtx struct {
    cancelCtx
    timer *time.Timer
    deadline time.Time
}