在go服务端,每个传入的 request 都在自己的 goroutine
中做后续处理。
request handlers 经常启动其他 goroutines
以访问后端,如数据库和rpc服务。
服务于 request 的一组常用典型的 goroutines
访问特定的请求值,例如最终用户的身份,授权令牌和请求的截止日期。
当 request 被取消或触发超时时,在该 request 上工作的所有 goroutine
应该快速退出,以便系统可以回收所使用的任何资源。
在google内部,开发了一个 context 包,可以轻松地跨越api边界,传递请求范围值,取消信号和截止日期到 request 所涉及的所有 goroutine
。
该包是开源的被称作 context。 本文介绍了如何使用该包并提供了一个完整的工作示例。
1 context
context 包的核心就是 context 类型(这里的描述是精简的,详情可见godoc):
1 | // a context carries a deadline, cancelation signal, and request-scoped values |
Done 方法返回一个 channel ,用于发送取消信号(代表 Context 已关闭)到运行时函数:当 channel
关闭时,函数应该放弃后续流程并返回。
Err 方法返回一个错误,指出为什么 context 被取消。 管道和取消文章更详细地讨论了 done channel 的惯用法。
由于 Done channel 只接收的原因,/Context/ 没有取消方法:接收取消信号的函数通常不应当具备发送信号的功能。
特别是,当父操作启动子操作的 goroutines
时,这些子操作不应该能够取消父操作。
相反, WithCancel 函数(如下所述)提供了一种取消新的 Context 值的方法。
Context 可以安全地同时用于多个 goroutines
。
代码可以将单个 Context 传递给任意数量的 goroutine
,并能发送取消该Context的信号到所有的关联的 goroutine
。
Deadline 方法允许功能确定是否应该开始工作; 如果剩下的时间太少,可能不值得。 代码中也可能会使用截止时间来为I/O操作设置超时。
Value 允许 Context 传送请求数据。 该数据必须能安全的同时用于多个 goroutine
。
2 Context的衍生
context/包提供了从现有 /Context 衍生出新的 Context 的函数。 这些 Context 形成一个树状的层级结构:当一个 Context 被取消时,从它衍生出的所有 Context 也被取消。
Background 是任何Context树的根; 它永远不会被取消:
1 | // Background returns an empty Context. It is never canceled, has no deadline, |
WithCancel 和 WithTimeout 返回衍生出的 Context ,衍生出的子 Context 可早于父 Context 被取消。 与传入的 request 相关联的上下文通常在请求处理程序返回时被取消。 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的Value相关方法只应该用于在程序和接口中传递的和请求相关的元数据,不要用它来传递一些可选的参数;
掌握如何使用 context 包的最佳方法是通过一个真实完整的示例。
3 Context使用的简单示例
简单的示例,更容易理解 Context 各衍生函数适用的场景,而且编辑本文档使用的是 Org-mode, 在编辑的过程中,即可执行(对org-mode感兴趣的人,可在评论里联系我)。 这里的代码,来源于 context 的godoc。
3.1 WithCancel
WithCancel 的示例, 演示如何使用可取消 context 来防止 goroutine
泄漏。
示例函数的结尾,由gen启动的goroutine将返回而不会发送泄漏。
1 | package main |
3.2 WithDeadline
WithDeadline 的示例,通过一个截止日期的 Context 来告知一个阻塞的函数,一旦它到了最终期限,就放弃它的工作。
1 | package main |
3.3 Withtimeount
WithTimeount 的示例, 传递具有超时的 Context 以告知阻塞函数,它将在超时过后丢弃其工作。
1 | package main |
3.4 WithValue
WithValue 的简单示例代码:
1 | package main |
4 示例:Google Web Search
示例是一个HTTP服务器,通过将查询“golang”转发到 Google Web Search API 并渲染查询结果, 来处理 "/search?q=golang&timeout=1s" 之类的URL。 timeout参数告诉服务器在该时间过去之后取消请求。
示例代码被拆分为三个包:
4.1 server
服务器通过为 golang 提供前几个 Google 搜索结果来处理像 "search?q=golang" 之类的请求。 它注册 /handleSearch 来处理 "search"。 处理函数创建一个名为ctx的 /Context ,并在处理程序返回时,一并被取消。 如果 request 包含超时URL参数,则超时时会自动取消上下文:
1 | func handleSearch(w http.ResponseWriter, req *http.Request) { |
处理程序从 request 中提取查询关键字,并通过调用 userip 包来提取客户端的IP地址。 后端请求需要客户端的IP地址,因此handleSearch将其附加到ctx:
1 | // Check the search query. |
处理程序使用ctx和查询关键字调用 google.Search :
1 | // Run the Google search and print the results. |
如果搜索成功,处理程序将渲染返回结果:
1 | if err := resultsTemplate.Execute(w, struct { |
4.2 userip
userip包提供从请求中提取用户IP地址并将其与 Context 相关联的函数。
Context 提供了 key-value 映射的 map ,其中 key 和 value 均为 interface{}
类型。
key 类型必须支持相等性, value 必须是多个 goroutine
安全的。
userip 这样的包会隐藏 map 的细节,并提供强类型访问特定的 Context 值。
为了避免关键字冲突, userip 定义了一个不导出的类型 key ,并使用此类型的值作为 Context 的关键字:
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) { |
4.3 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 ,取消 httpDo 。 Search 将传递闭包给 httpDo 来处理HTTP响应:
1 | var results Results |
httpDo 函数发起HTTP请求,并在新的 goroutine
中处理其响应。
如果在 goroutine
退出之前关闭了ctx.Done,它将取消该请求:
1 | func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error { |
5 适配Context到已有代码
许多服务器框架提供用于承载请求范围值的包和类型。 可以定义 Context 接口的新实现,以便使得现有的框架和期望Context参数的代码进行适配。
例如,Gorilla的 github.com/gorilla/context 包允许处理程序通过提供从HTTP请求到键值对的映射来将数据与传入的请求相关联。 在 gorilla.go 中,提供了一个 Context 实现,其 Value 方法返回与 Gorilla 包中的特定HTTP请求相关联的值。
其他软件包提供了类似于 Context 的取消支持。
例如,Tomb 提供了一种杀死方法,通过关闭死亡 channel
来发出取消信号。
Tomb还提供了等待 goroutine
退出的方法,类似于sync.WaitGroup。
在 tomb.go 中,提供一个 Context 实现,当其父 Context 被取消或提供的 Tomb 被杀死时,该 Context 被取消。
6 总结
在Google,我们要求Go程序员通过 Context 参数作为传入和传出请求之间的呼叫路径上每个函数的第一个参数。 这允许由许多不同团队开发的Go代码进行良好的互操作。 它提供对超时和取消的简单控制,并确保安全证书等关键值正确转移Go程序。
希望在 Context 上构建的服务器框架应该提供 Context 的实现,以便在它们的包之间和期望 Context 参数的包之间进行适配。 客户端库将接受来自调用代码的 Context 。 通过为请求范围的数据和取消建立通用接口, Context 使得开发人员更容易地共享用于创建可扩展服务的代码。
Render by hexo-renderer-org with Emacs 25.3.2 (Org mode 8.2.10)