大家好,我是江湖十年。
Go 官方博客在 2014 年发表过一篇介绍 context 包的文章《Go Concurrency Patterns: Context》。虽然年代久远,但文中对 Context 的设计动机、接口语义和使用方式的讲解依然经典。本文是对这篇文章的中文翻译。
如下为译文:
Sameer Ajmani
2014 年 7 月 29 日
引言
在 Go 服务器中,每个传入请求都由独立的 goroutine 处理。请求处理程序通常会启动额外的 goroutine 来访问数据库、RPC 服务等后端。处理某个请求的一组 goroutine 通常需要访问请求级的值,例如终端用户的身份、授权令牌以及请求的截止时间。当请求被取消或超时时,处理该请求的所有 goroutine 都应迅速退出,以便系统回收它们正在使用的资源。
在 Google,我们开发了 context 包,它可以轻松地将请求级的值、取消信号和截止时间跨 API 边界传递给处理请求所涉及的所有 goroutine。该包现已作为 context 公开提供。本文介绍如何使用该包,并给出一个完整可运行的示例。
Context
context 包的核心是 Context 类型:
1 | // A Context carries a deadline, cancellation signal, and request-scoped values |
(以上描述经过精简;以 godoc 为准。)
Done 方法返回一个 channel,它充当取消信号,通知所有代表该 Context 执行工作的函数:一旦该 channel 关闭,这些函数就应放弃手头的工作并返回。Err 方法返回一个错误,说明 Context 被取消的原因。《Pipelines and Cancellation》一文更详细地讨论了 Done channel 惯用法。
Context 并没有 Cancel 方法,原因与 Done channel 只能接收相同:接收取消信号的函数通常并非发送信号的那一方。特别是,当父操作启动子操作的 goroutine 时,这些子操作不应能够取消父操作。相反,WithCancel 函数(下文介绍)提供了一种取消新的 Context 值的方法。
Context 可以安全地被多个 goroutine 同时使用。代码可以把同一个 Context 传给任意数量的 goroutine,并通过取消该 Context 来向它们全部发出信号。
Deadline 方法让函数能够判断自己是否应该开始工作;如果剩余时间太少,可能就不值得做了。代码也可以使用截止时间来为 I/O 操作设置超时。
Value 允许 Context 携带请求级数据。这些数据必须能安全地被多个 goroutine 同时使用。
派生 Context
context 包提供了从现有 Context 值派生出新 Context 值的函数。这些值构成一棵树:当某个 Context 被取消时,由它派生的所有 Contexts 也会被取消。
Background 是任何 Context 树的根;它永远不会被取消:
1 | // Background returns an empty Context. It is never canceled, has no deadline, |
WithCancel 和 WithTimeout 返回派生的 Context 值,它们可以比父 Context 更早被取消。与传入请求关联的 Context 通常会在请求处理程序返回时被取消。在使用多个副本时,WithCancel 还可用于取消冗余请求。WithTimeout 适合为发往后端服务器的请求设置截止时间:
1 | // WithCancel returns a copy of parent whose Done channel is closed as soon as |
WithValue 提供了一种将请求级值与 Context 关联的方法:
1 | // WithValue returns a copy of parent whose Value method returns val for key. |
了解如何使用 context 包的最佳方式,是通过一个完整的示例。
示例:Google 网页搜索
我们的示例是一个 HTTP 服务器,它处理类似 /search?q=golang&timeout=1s 的 URL:把查询 “golang” 转发给 Google Web Search API,并渲染结果。timeout 参数告诉服务器在该时长过后取消请求。
代码分为三个包:
- server 提供
main函数和/search的处理程序。 - userip 提供从请求中提取用户 IP 地址并将其与
Context关联的函数。 - google 提供向 Google 发送查询的
Search函数。
server 程序
server 程序处理类似 /search?q=golang 的请求,返回 golang 的前几条 Google 搜索结果。它注册 handleSearch 来处理 /search 端点。该处理程序创建一个初始的 Context,名为 ctx,并安排在处理程序返回时取消它。如果请求包含 timeout URL 参数,则 Context 会在超时后自动取消:
1 | func handleSearch(w http.ResponseWriter, req *http.Request) { |
处理程序从请求中提取 query,并调用 userip 包提取客户端 IP 地址。后端请求需要客户端 IP 地址,因此 handleSearch 将它附加到 ctx 上:
1 | // Check the search query. |
处理程序调用 google.Search,并传入 ctx 和 query:
1 | // Run the Google search and print the results. |
如果搜索成功,处理程序会渲染结果:
1 | if err := resultsTemplate.Execute(w, struct { |
userip 包
userip 包提供从请求中提取用户 IP 地址并将其与 Context 关联的函数。Context 提供键值映射,其中键和值的类型都是 interface{}。键类型必须支持相等比较,值必须能安全地被多个 goroutine 同时使用。像 userip 这样的包会隐藏这种映射的细节,并为特定的 Context 值提供强类型访问。
为了避免键冲突,userip 定义了一个未导出的类型 key,并使用该类型的值作为 context key:
1 | // The key type is unexported to prevent collisions with context keys defined in |
FromRequest 从 http.Request 中提取 userIP 值:
1 | func FromRequest(req *http.Request) (net.IP, error) { |
NewContext 返回一个携带所提供 userIP 值的新 Context:
1 | func NewContext(ctx context.Context, userIP net.IP) context.Context { |
FromContext 从 Context 中提取 userIP:
1 | func FromContext(ctx context.Context) (net.IP, bool) { |
google 包
google.Search 函数向 Google Web Search API 发起 HTTP 请求,并解析 JSON 编码的结果。它接受一个 Context 参数 ctx,如果请求进行期间 ctx.Done 关闭,则立即返回。
Google Web Search API 请求将搜索查询和用户 IP 作为查询参数:
1 | func Search(ctx context.Context, query string) (Results, error) { |
Search 使用辅助函数 httpDo 发起 HTTP 请求,并在请求或响应处理期间 ctx.Done 关闭时取消该请求。Search 把一个闭包传给 httpDo 来处理 HTTP 响应:
1 | var results Results |
httpDo 函数在新的 goroutine 中运行 HTTP 请求并处理其响应。如果 goroutine 退出前 ctx.Done 关闭,它会取消该请求:
1 | func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error { |
让现有代码适配 Context
许多服务器框架提供用于携带请求级值的包和类型。我们可以定义 Context 接口的新实现,在使用现有框架的代码和期望 Context 参数的代码之间架起桥梁。
例如,Gorilla 的 github.com/gorilla/context 包通过提供从 HTTP 请求到键值对的映射,让处理程序能够把数据与传入请求关联起来。在 gorilla.go 中,我们提供了一个 Context 实现,其 Value 方法返回与 Gorilla 包中某个特定 HTTP 请求关联的值。
其他包也提供了类似 Context 的取消支持。例如,Tomb 提供了一个 Kill 方法,它通过关闭 Dying channel 来发出取消信号。Tomb 还提供了等待这些 goroutine 退出的方法,类似于 sync.WaitGroup。在 tomb.go 中,我们提供了一个 Context 实现,当它的父 Context 被取消或传入的 Tomb 被杀死时,该实现会被取消。
结论
在 Google,我们要求 Go 程序员在传入请求和传出请求之间的调用路径上,把 Context 参数作为每个函数的第一个参数。这使许多不同团队开发的 Go 代码能够良好地互操作。它提供了对超时和取消的简单控制,并确保安全凭证等关键值能正确地在 Go 程序中传递。
想要基于 Context 构建的服务器框架,应提供 Context 的实现,以便在它们自己的包与期望 Context 参数的包之间架起桥梁。它们的客户端库随后会从调用代码接收 Context。通过为请求级数据和取消建立通用接口,Context 让包开发者更容易共享用于构建可扩展服务的代码。
原文完。
希望此文能对你有所启发。
延伸阅读
- Go Concurrency Patterns: Context 原文:https://go.dev/blog/context
- Go Concurrency Patterns: Pipelines and cancellation:https://go.dev/blog/pipelines
- Contexts and structs:https://go.dev/blog/context-and-structs
- Go context 包文档:https://pkg.go.dev/context
- 本文永久地址:https://jianghushinian.cn/2026/11/03/go-concurrency-patterns-context/
联系我
- 公众号:Go编程世界
- 微信:jianghushinian
- 邮箱:jianghushinian007@outlook.com
- 博客:https://jianghushinian.cn
- GitHub:https://github.com/jianghushinian