这篇文章详细介绍了Go 语言中context 函数背后的实现细节和代码,帮助开发人员了解上下文包的底层工作原理。
我们来看一个使用 context 包的简单示例:该函数接受一个上下文并将其传递给另一个函数,因为对于大多数人来说,这就是上下文的全部,只是在函数需要时传递的东西。
func main() { |
将打印出 context.Background。这是因为 context.Background 返回的内容满足 Stringer 接口的要求,而 Stringer 接口在调用 String.Background 时只会返回该内容。
context接口
让我们从最基本的开始。我们使用的 context.Context 类型是一个接口,下面是它的定义。
type Context interface { |
任何满足此接口的结构都是有效的上下文对象。如果注释中没有说明,让我们快速了解一下它们分别是什么。
- Deadline:该函数返回设置为截止日期的时间,例如,如果上下文是使用 context.WithDeadline 创建的。
- Done:该函数返回一个在取消上下文时关闭的通道。
- Err:如果取消已发生,则返回非零。
- Value:值:该函数用于获取存储在上下文实例中的值。
如果你想创建一个 "Context",这些就是你所需要的,而且你可以很容易地创建它们。尽管如此,stdlib 还是为我们提供了一些有用的 "上下文"。
emptyCtx 结构
这是一个结构体,满足成为 Context 的最基本要求。代码如下
type emptyCtx struct{} |
如您所见,它什么也不做,但这就是 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 } |
context.TODO:
type todoCtx struct{ emptyCtx } |
context.WithValue
现在,我们将进入 context 软件包的更多实用案例。如果想使用 context 传递一个值,可以使用 context.WithValue。您可能见过日志或网络框架使用这种方法。
让我们看看它的内部结构:
type valueCtx struct { |
它只是返回一个包含父上下文、键和值的结构体。
如果你注意到,该实例只能包含一个键和一个值,但你可能在网络框架中看到过,它们会从 ctx 参数中提取多个值。由于我们将父上下文嵌入到了较新的上下文中,因此可以向上递归搜索以获取其他任何值。
bgCtx := context.Background() |
现在,如果我们要从 v2Ctx 中获取 "one "的值,可以调用 v2Ctx.Value("one")。
这将首先检查 v2Ctx 中的键是否为 "one",
如果不是,则将检查父节点(v1Ctx)的键是否为 "one"。
既然 v1Ctx 中的键是 "one",我们就返回上下文中的值。
下面代码递归搜索父上下文,查看其中是否有匹配键的上下文,然后返回其值。
func (c *valueCtx) Value(key any) any { |
context.WithCancel
让我们来看看更有用的东西。您可以使用上下文包创建一个 ctx,用来向下游函数发出取消信号。
让我们来看一个如何使用的示例:
func doWork(ctx context.Context) { |
在这种情况下,您可以通过在主函数中调用 cancel 来向 doWork 函数发出停止工作的信号。
现在来看看它是如何工作的。让我们从函数定义开始(我们很快就会讲到结构定义):
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { |
当您调用 context.WithCancel 时,它会返回一个 cancelCtx 实例和一个函数,您可以调用该函数来取消上下文。由此我们可以推断,cancelCtx 是一个具有取消函数的上下文,该函数可用来 "取消 "上下文。
如果你忘记了,取消上下文只是意味着关闭 Done() 返回的通道。
顺便说一下,在此上下文中,propagateCancel 函数的主要作用是创建一个 cancelCtx,以及在创建之前确保父节点尚未被取消。
好了,现在让我们来看看结构图,之后我们将了解它是如何工作的(source).
type cancelCtx struct { |
这里:
- Context:保存父上下文
- mu sync.Mutex:你知道这是什么吧
- done atomic.Value:保存将由 Done() 函数返回的 chan struct{}
- err error:保存导致取消的错误信息
- cause error:保存取消的原因,即取消函数的最后一个参数
对了,这是取消功能(source):
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) { |
首先,我们在实例上设置原因和错误,然后关闭 Done 返回的通道。之后,它会取消所有子实例,最后将自己从父实例中移除。
Context.WithDeadline 和context.WithTimeout
当您想创建一个在到达截止日期时自动取消的上下文时,这些功能就非常有用。这对于执行服务器超时等操作非常有用。
首先,context.WithTimeout 只是计算截止时间并调用 context.WithDeadline。事实上,这就是它的全部代码:
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { |
现在进入细节部分。有些人可能已经猜到,WithDeadline 基本上就是一个普通的 WithCancel 上下文,只不过是由上下文包来处理取消。
让我们看看代码的作用。这里有函数 WithDeadlineCause 的代码,它是 WithDeadline 的一个变体,但增加了传递取消的 "原因 "的功能。顺便提一下,其他上下文包函数也可以使用 Cause 变体,而像 WithDeadline 这样的非 Cause 变体只是在调用 Cause 变体时将 cause 设为 nil。
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) { |
- 如果父节点的截止日期早于子节点,则返回从父节点创建的简单 cancelCtx
- 如果不是,则创建一个新的 timeCtx(结构定义如下)
- 现在检查是否已超过截止时间,如果是,则取消已创建的上下文并返回
- 如果没有,我们将使用 time.AfterFunc 设置一个计时器来执行取消操作,然后返回
timerCtx 只是一个带有计时器的 cancelCtx:
type timerCtx struct { |