深入理解 Go 的 Context 包
- Published on
Context
包的开发目的是为了让在 API 边界和进程之间传递请求域的值、截止时间和取消信号变得简单。如果我们先从非技术角度理解 Context("形成事件、陈述或想法的环境背景,并据此可以完全理解它"),可能会帮助我们更好地理解技术层面的 Context。Context
包在处理服务器、发出 HTTP 请求以及许多其他涉及 goroutine 的活动时非常有用。本文旨在深入解释 Go 提供的 context
包。在接下来的章节中,我们将深入探讨 context
包。
Context 类型
context
包的主要组件是 Context
类型。我们可以从Go 文档中了解更多关于这个类型的信息。仔细观察可以发现,Context
类型是一个实现了四个函数的接口:
deadline
函数done
函数error
函数value
函数
这些函数是我们使用 context
包进行大多数操作的基础,我们稍后会看到它们的应用。重要的是要知道,context
包为我们提供了作为 context
包构建块的 Context
类型。context
包中的第二个类型是 CancelFunc
类型,它是一个函数。CancelFunc
用于取消操作,它不会等待这些操作停止后再取消,而是告诉操作立即放弃它们正在做的事情。
Context 实战
context
包允许我们使用它提供的几个函数从已存在的 context 值创建新的 context 值!这些函数包括:
WithCancel
WithTimeout
WithDeadline
它们都返回两个值,分别是 Context
类型和 cancelFunc
类型。这些函数返回的值称为派生值,它们通常形成一个以 Background
函数提供的 context 为根的树。当一个 context 被取消时,从它派生的所有值都会自动被取消。 Background
函数通常用于初始化目的,因为它返回一个非空的空 context,并且永远不会被取消,即使 WithCancel
、WithTimeout
和 WithDeadline
函数返回的子 context 可以被取消并删除其对 Background context 的引用。

这些派生 context 值形成的树允许取消传播,即如果一个 context 被取消,从它派生的所有 context 也会被取消。这一点很重要,因为取消是 context
包最常见的用途之一。
在使用 context 时有一个重要原则:避免将其存储在结构体中,因为这可能会影响程序的同步性质。将 context 存储在结构体中意味着该结构体的所有方法都使用该 context,当这些方法从具有不同 context 的函数调用时,可能会导致混乱。记住,context 值应该是请求域的。
确保显式地将 Context
作为参数传递给接受它的函数,传递给函数的 context 通常应该是第一个参数,并且不为 nil
。**不应该向函数传递 nil
context。**如果你不知道当前 context 的值,可以传递 ToDo
函数的值。与 Background
函数类似,ToDo
函数返回一个非 nil 的空 context,当不确定使用什么 context 时或作为占位符时使用,直到周围的函数收到 context。Background
和 ToDo
函数为派生更多 Context 值提供了基础。
WithValue 函数
WithValue
函数顾名思义是用于处理值的,这些值通常是请求域的,典型的用于在进程和 API 之间传递的数据。WithValue
函数不应该用于向函数传递参数。
WithValue
函数定义如下:
func WithValue(parent Context, key, val interface{}) Context
它接收一个父/根 context、一个键和要与键关联的值。它返回一个包含该键值的 Context
,其工作方式类似于键值对模型。重要的是要注意键不应该是任何内置类型,它应该是一个自定义类型,这是为了避免使用 context 的包之间的冲突。
让我们看一个例子:
package main
import (
"fmt"
"context"
)
type keyType string
func main() {
key := keyType("Name")
ctx := context.WithValue(context.Background(), key, "Tobyy")
exampleContext(ctx, key)
}
func exampleContext(ctx context.Context, k keyType){
value := ctx.Value(k)
if value != nil {
fmt.Print("The context value is :", value)
return
}
fmt.Print("Ooooops, unable to find the context value")
}
上面的示例是一个简单的程序,包含两个函数:默认的 main
函数和第二个自定义函数 exampleContext
。exampleContext
函数期望一个 context 作为其第一个参数,以及 context 值的键。请注意,context 值的类型不是内置类型,正如我们之前讨论的那样。exampleContext
函数的目的是检查提供的 context 中是否存在与提供的 key 相对应的值。如果值存在,我们就打印出这个值;否则,我们向终端打印一条消息说明找不到 context 值。很简单对吧?
真正有趣的部分发生在 main
函数中。在 main 函数中,我们创建了一个类型为 keyType
的变量(keyType
是我们创建的自定义类型)。在这个例子中,我为了清晰起见将变量命名为 key
,但你可以根据需要为它取任何名字,这个变量随后会被传递给 exampleContext
函数。
exampleContext
函数期望的第二个参数是 Context
类型的 context。我们在 main 函数的第二行创建了这个 context。这个 context 是使用 WithValue
函数创建的,因为我们要处理的是值。如果你还记得前面提到的,WithValue
函数需要一个父 Context 作为其参数之一,所以我们将从 Background
函数获取的 background context 作为第一个参数传递给 exampleContext
。
我们可以将对 context.Background
的调用抽象成一个变量并将其传递给 WithValue
函数,但为了简洁起见,我们直接在参数中调用它。WithValue
函数期望的下一个参数是 key,所以我们将上面创建的 key 作为参数传入。WithValue
函数期望的另一个参数是我们想要附加到 key 上的值本身。在这个简单的例子中,我传入字符串 "Toby"
作为值。
最后,我们将创建的 context 作为参数传递给 exampleContext
函数,然后就可以运行它了。
The context value is: Toby
如果我们运行程序,会得到输出值!WithValue
函数对于传递值非常重要,特别是请求域的值。 WithValue
函数最常见的用例之一是在发出 HTTP 请求时。Go 的 net/Http
包在底层有机制为每个请求创建一个 context,可以通过 Context()
方法访问,所以通常会看到 r.Context()
,其中 r
是 http.Request
类型。
WithCancel 函数
获取派生 context 的另一种方法是使用 WithCancel
函数。WithCancel
函数就像其他用于派生 context 的函数一样,返回一个子 context 和一个取消函数。但是,WithCancel
函数返回的子 context 带有一个新的 Done()
通道。记住我们之前讨论过 Context 接口,看到它有一个 Done()
函数?返回的 context 的 Done 通道在返回的 cancel 函数被调用或其父 context 的 Done 通道关闭时关闭,具体取决于哪个先发生。每当 Done 通道关闭时,context.Err
函数会返回一个错误消息 "Context Cancelled"。
让我们看一个例子:
package main
import (
"fmt"
"context"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
time.AfterFunc(2*time.Second, cancel)
sayMyName(ctx, 5*time.Second, "Toby")
}
func sayMyName(ctx context.Context, d time.Duration, name string){
select {
case <- time.After(d):
fmt.Print("Your name is ", name)
case <-ctx.Done():
err := ctx.Err()
fmt.Print(err)
}
}
上述示例是一个常用来解释 Go 中 Context 概念的例子,其中包含两个函数:
第一个是默认的
main
函数。第二个是自定义函数
sayMyName
它接收三个参数:
- 类型为
Context
的 Context 参数 - 类型为
time.Duration
的持续时间参数 - 类型为
string
的名字参数
- 类型为
sayMyName
函数的目标是:在提供的持续时间后打印出您的名字,前提是 Context 未被取消。在 sayMyName
函数中,使用了一个 select
语句。在 Go 中,select
用于让 goroutine 等待多个通信操作。select
会阻塞,直到其中一个 case 可以执行,然后执行该 case。
在上述示例中,select
语句检查两个 case:
- 第一个 case:检查提供的持续时间是否已过。为此,使用
time.After
函数,该函数接收一个持续时间参数,等待时间过去后,会在返回的通道中发送一条消息。 - 第二个 case:检查
Done
函数是否返回了一个关闭的通道。记住我们之前讨论的,当通过WithCancel
函数返回的cancel
被调用时,Done
通道会被关闭。
只要两个 case 中的任何一个能够运行,select
就会执行相应的 case。
在回到 main
函数时,我们调用了 sayMyName
函数,而它的一个参数是 Context。因此,我们使用 WithCancel
函数并传入 Background
Context 作为其根 Context。WithCancel
函数返回了一个新的 Context 和一个 cancel
,于是将它们存储到变量中。
接下来,我们将派生的 Context 与 5 秒的持续时间和字符串 "Toby"
一起传递给 sayMyName
函数。
最后,我们需要调用 WithCancel
函数返回的 cancel
,为此使用了 time
包提供的 AfterFunc
函数。
AfterFunc
函数的作用是:在指定的时间过去后调用所传入的函数。在这个例子中,我们设置了 2 秒的延迟来模拟取消操作。
至此,我们完成了函数的设置,可以运行代码。运行后输出:
context canceled
运行结果如预期那样,传入的字符串并没有被打印出来!这是因为我们在 2 秒后取消了该函数,而名字的打印需要等待 5 秒。很神奇对吧?这就是 context
包赋予我们的强大功能:能够实现任务的取消和取消传播。
这种能力在处理服务器请求和网络通信时尤其重要。例如,用户可以在收到响应之前决定取消其请求,而通过 context
的取消机制,这个过程将会优雅地完成,因为取消 Context 会释放与之相关的资源。
WithTimeout
函数可以被看作是 增强版的 WithCancel
函数。我们将在下一节中详细探讨它的用法。
WithTimeout 函数
WithTimeout
基本上是加强版的 WithCancel
函数。让我们看一个例子:
package main
import (
"fmt"
"context"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
sayMyName(ctx, 5*time.Second, "Toby")
}
func sayMyName(ctx context.Context, d time.Duration, name string){
select {
case <- time.After(d):
fmt.Print("Your name is ", name)
case <-ctx.Done():
err := ctx.Err()
fmt.Print(err)
}
}
上面的示例与前一部分的例子非常相似。然而,在这个示例中,我们使用的是 WithTimeout
函数。
WithTimeout
函数 通常用于向服务器发送 HTTP 请求或执行类似操作。它接收一个超时时间作为参数,在超时时间结束后,done
通道会被关闭,Context 也会被取消。它返回一个新的子 Context 和一个 cancel
函数,如果需要在超时之前取消 Context,可以调用该函数。
在 main
函数的第二行中,我们通过传入 Background
Context 和持续时间(此处为 2 秒)派生了一个新的 Context。在下一行中,我们使用 defer
语句调用 cancel
函数!
您可能注意到,在这个示例中,AfterFunc
函数消失了,这是因为 WithTimeout
函数已经优雅地替代了 AfterFunc
的功能。如果我们运行这个示例,将会得到如下输出:
context deadline exceeded
注意:错误信息发生了变化!这个错误与我们之前提到的 context
接口中的 Deadline
函数相关联。
总结
context
包为 Go 程序提供了许多重要功能。它帮助我们处理超时、取消传播等复杂操作。