[译]Go 并发 : Context
翻译自:Go Concurrency Patterns: Context
地址:https://blog.golang.org/context
原文发表于 2014 年。context 在 Go 1.7 中进入标准库,文中的 Google 网页搜索 API 已停用。下文保留原例来说明 Context 的用法,代码片段需结合链接中的完整源码阅读。
简介
在Go server端,每个请求都是通过新起goroutine来处理。请求处理程序通常会启动其他goroutine来访问后端,例如数据库和RPC服务。处理该请求的goroutine集合通常需要访问特定于请求的值,例如最终用户的身份,授权令牌和请求的截止时间(deadline)。当一个请求被取消或超时时,处理该请求的所有goroutine应该迅速退出,以便系统可以回收它们正在使用的资源。
在Google,我们开发了一个 context 包,可以轻松地跨API边界将请求范围(request-scoped)的值,取消信号和截止时间(deadlines)传递给处理请求的所有goroutine。该包以 context 之名公开发布。本文将介绍如何使用该包,并通过一个 HTTP 服务示例说明它的用法。
Context
context 包的核心是Context类型:
1 | // Context跨API边界携带截止时间(deadline)、取消信号和请求范围(request-scoped)的值。Context的方法是协程安全的。 |
更详细的介绍,参见godoc:https://golang.org/pkg/context/
Done方法返回一个channel,作为代表Context运行的函数的取消信号:当channel关闭时,函数应该放弃它们的工作并返回。Err方法返回一个错误,指示Context被取消的原因。《Go Concurrency Patterns: Pipelines and Cancellation》一文进一步讨论了 Done channel 的惯用法。
Context 没有 Cancel 方法,原因与 Done channel 只能接收是一样的:接收取消信号的方法与发送信号方法往往不是同一个。特别是,当父操作(parent operation)为子操作(sub-operations)启动goroutines时,这些子操作应该不能取消父操作。相反,WithCancel函数(如下所述)提供了一种取消新的 Context 值的方法。
Context是协程安全的。代码中可以将单个Context传递给任意数量的goroutine,并在取消该Context时可以将信号传递给所有的goroutine。
截止时间(Deadline)方法允许函数判断是否应该开始工作;如果剩下的时间太少,可能就不值得了。代码也可以使用截止时间(Deadline)来设置I/O操作的超时。
值(Value)允许Context携带请求范围(request-scoped)的数据。 该数据必须是协程安全的,以便多个goroutine可以同时使用。
Derived contexts
context 包提供了从现有Context值派生新Context值的函数。这些值形成一个树:当一个Context被取消时,从它派生的所有Context也被取消。
Background是所有Context树的根;它永远不会被取消:
1 | // Background返回一个空Context。它永远不会被取消,没有截止时间,也没有值。 |
WithCancel和WithTimeout返回派生的Context值,该值可以比父Context早被取消。请求处理程序返回时,通常会取消与传入请求关联的Context。 使用多个副本时,WithCancel对于取消冗余请求也很有用。WithTimeout对于设置对后端服务器的请求的截止时间很有用:
1 | // WithCancel 返回 parent 的副本,其 Done channel 会在 parent.Done 关闭或调用 cancel 时立即关闭。 |
WithValue提供了一种将请求范围的值与Context关联的方法:
1 | // WithValue 返回 parent 的副本,其 Value 方法对给定的 key 返回 val。 |
最好的理解方式是通过一个完整的示例来看 context 包的用法。
示例:Google 网页搜索
我们的示例是一个 HTTP 服务器,处理形如 /search?q=golang&timeout=1s 的 URL:它把查询词 “golang” 转发给 Google 网页搜索 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) { |
处理函数从请求中提取查询词,并调用 userip 包提取客户端 IP 地址。后端请求需要客户端 IP,因此 handleSearch 将其附加到 ctx 上:
1 | // 检查搜索查询词。 |
处理函数用 ctx 和查询词调用 google.Search:
1 | // 执行 Google 搜索并打印结果。 |
如果搜索成功,处理函数渲染结果:
1 | if err := resultsTemplate.Execute(w, struct { |
userip 包
userip 包提供从请求中提取用户 IP 地址、并将其与 Context 关联的函数。Context 提供键值映射,键和值都是 interface{} 类型:键类型必须支持相等比较,值必须对多个 goroutine 并发安全。像 userip 这样的包隐藏了这个映射的细节,提供对特定 Context 值的强类型访问。为避免键冲突,userip 定义了一个未导出的 key 类型,并以该类型的值作为 context 键:
1 | // key 类型未导出,以避免与其他包中定义的 context 键冲突。 |
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 网页搜索 API 发起 HTTP 请求,并解析 JSON 编码的结果。它接受一个 Context 参数 ctx,如果请求在途时 ctx.Done 被关闭,则立即返回。Google 网页搜索 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 被 kill 时,它即被取消。
结论
在 Google,我们要求 Go 程序员在进出请求之间调用路径上的每个函数中,都把 Context 作为第一个参数传递。这使得由许多不同团队开发的 Go 代码能够良好地协同工作。它提供了对超时和取消的简单控制,并确保安全凭证等关键值能够正确地穿过 Go 程序。
想要基于 Context 构建的服务框架,应当提供 Context 的实现,在自己的包与期望 Context 参数的包之间架起桥梁;它们的客户端库随后即可接受调用方传入的 Context。通过为请求范围的数据和取消建立一个共同的接口,Context 让包开发者更容易共享构建可扩展服务的代码。
延伸阅读
- Go Concurrency Patterns: Pipelines and Cancellation(2014 年博文)
- Contexts and structs(2021 年博文)
📚 系列文章 · Go 并发与运行时