go context包官方文档
Go并发模式:Context
在Go服务器中,每个传入的请求都在它自己的goroutine中处理。请求处理程序通常启动附加的goroutine来访问后端,例如数据库和RPC服务。在处理请求的一组goroutine通常需要访问请求特定的值,例如终端用户的身份,授权令牌和请求的截止时间。当请求被取消或超时时,所有正在处理该请求的goroutine应该快速退出,以便系统可以回收它们正在使用的任何资源。
在Google中,我们开发了一个Context包,使得在处理请求的所有涉及的goroutine之间跨API边界传递请求范围的值,取消信号和截止时间变得容易。该包作为context公开可用。本文介绍了如何使用该包并提供一个完整的工作示例。
Context
Context包的核心是Context类型:
1 | // Context 在 API 边界上携带截止时间、取消信号和请求范围的值。它的方法可以同时被多个 goroutine 安全使用。 |
Done方法返回一个通道(channel),作为代表Context运行的函数的取消信号:当通道关闭时,函数应该放弃它们的工作并返回。
Err方法返回指示Context被取消的原因的错误。Pipelines and Cancellation文章详细讨论了Done通道惯用法。
Context没有Cancel方法,原因与Done通道是只接收的相同:接收取消信号的函数通常不是发送信号的函数。特别是,当父操作为子操作启动goroutine时,这些子操作不应该能够取消父操作。相反,WithCancel函数(下文将描述)提供了一种取消新Context值的方式。
Context对于多个goroutine的同时使用是安全的。代码可以将单个Context传递给任意数量的goroutine并取消该Context以通知它们所有。
Deadline方法允许函数确定它们是否应该开始工作;如果剩下的时间太少,则可能不值得。代码也可以使用截止时间为I/O操作设置超时。
Value允许Context携带请求范围的数据。该数据必须对多个goroutine的同时使用是安全的。
派生Context
context包提供了从现有Context派生新Context值的函数。这些值形成一棵树:当Context被取消时,由它派生的所有Context也被取消。
Background是任何Context树的根;它从不被取消:
1 | // Background 返回一个空的 Context。它从不被取消,没有截止时间, |
WithCancel 和 WithTimeout 返回派生上下文值,可以更快地取消父上下文。与传入请求关联的上下文通常在请求处理程序返回时取消。在使用多个副本时,WithCancel 也有助于取消冗余请求。WithTimeout 有助于为后端服务器上的请求设置截止时间:
1 | // WithCancel 返回 parent 的副本,其 Done 通道在 |
WithValue 提供了一种将请求范围的值与上下文关联的方法:
1 | // WithValue 返回 parent 的副本,其 Value 方法对于 key 返回 val。 |
最好的了解上下文(context)包的方法是通过一个实例来进行详细讲解。
示例:谷歌网页搜索
我们的示例是一个 HTTP 服务器,处理像 /search?q=golang&timeout=1s 这样的 URL,通过将查询“golang”转发到 Google Web 搜索 API 并呈现结果来处理请求。timeout 参数告诉服务器在该持续时间过后取消请求。
代码分为三个包:
- server 提供主函数和处理 /search 的处理程序。
- userip 提供从请求中提取用户 IP 地址并将其与上下文关联的函数。
- google 提供 Search 函数以向 Google 发送查询。
示例程序
服务通过提供 golang 的前几个 Google 搜索结果来处理请求,例如 /search?q=golang。它注册 handleSearch 来处理 /search 终端节点。处理程序创建一个名为 ctx 的初始上下文,并安排在处理程序返回时取消它。如果请求包括 timeout URL 参数,则在超时时间到期时自动取消上下文:
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 地址并将其与上下文关联的函数。上下文提供了一个键值映射,其中键和值都是 interface{} 类型。键类型必须支持相等性,值必须可以同时安全地用于多个 goroutine。像 userip 这样的包隐藏了此映射的细节,并提供了对特定上下文值的强类型访问。
为避免键冲突,userip 定义了一个未导出类型的键,并使用该类型的值作为上下文键:
1 | // 为了避免与其他包中定义的上下文键冲突,键类型是未导出的。 |
FromRequest 从 http.Request 中提取 userIP 值:
1 | func FromRequest(req *http.Request) (net.IP, error) { |
NewContext 返回一个携带所提供的 userIP 值的新上下文:
1 | func NewContext(ctx context.Context, userIP net.IP) context.Context { |
FromContext 从上下文中提取 userIP:
1 | func FromContext(ctx context.Context) (net.IP, bool) { |
包 google
google.Search 函数向 Google Web 搜索 API 发出 HTTP 请求并解析 JSON 编码的结果。它接受一个上下文参数 ctx,并在请求进行中 ctx.Done 关闭时立即返回。
Google Web 搜索 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 发出 HTTP 请求,并在请求或响应被处理时关闭 ctx.Done 来取消请求。搜索通过将一个闭包传递给 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 通道来信号取消。Tomb 还提供了一些方法来等待这些 goroutine 退出,类似于 sync.WaitGroup。在 tomb.go 中,我们提供了一个 Context 实现,当其父 Context 被取消或提供的 Tomb 被杀死时,它就会被取消。
结论 在 Google,我们要求 Go 程序员将 Context 参数作为进出请求调用路径上每个函数的第一个参数。这使得许多不同团队开发的 Go 代码可以很好地互操作。它提供了简单的超时和取消控制,并确保关键值如安全凭据适当地在 Go 程序之间传输。