Go语言并发模型:使用 context
2017-09-25 16:08
477 查看
出处:
https://segmentfault.com/a/1190000006744213
https://studygolang.com/articles/9517
在Google 内部,我们开发了 Context 包,专门用来简化 对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用。你可以通过 go get golang.org/x/net/context 命令获取这个包。本文要讲的就是如果使用这个包,同时也会提供一个完整的例子。
由于访问 golang.org/x/net/context 需要梯子,你可以访问它在 github 上的 mirror。
如果要下载本文中的代码,可以查看文章末尾的“相关链接”环节。
注意: 这里我们对描述进行了简化,更详细的描述查看 godoc:context
一个
对它执行 取消 操作时,所有 goroutine 都会接收到取消信号。
这些
当请求处理函数返回时,与该请求关联的
WithValue 函数能够将请求作用域的数据与 Context 对象建立关系。声明如下:
当然,想要知道 Context 包是如何工作的,最好的方法是看一个栗子。
Google Web Search API,然后渲染返回的结果。
这个例子的代码存放在三个包里:
server:它提供 main 函数和 处理
userip:它能够从 请求解析用户的IP,并将请求绑定到一个
google:它包含了 Search 函数,用来向 Google 发送请求。
###5.1 深入 server 程序
server 程序处理类似于 /search?q=golang 的请求,返回 Google API 的搜索结果。它将 handleSearch 函数注册到 /search 路由。处理函数创建一个 Context ctx,并对其进行初始化,以保证 Context 取消时,处理函数返回。如果请求的 URL 参数中包含 timeout,那么当 timeout 到期时, Context 会被自动取消。
handleSearch 的代码如下:
处理函数 (handleSearch) 将query 参数从请求中解析出来,然后通过 userip 包将client IP解析出来。这里 Client IP 在后端发送请求时要用到,所以 handleSearch 函数将它 attach 到 Context 对象 ctx 上。代码如下:
处理函数带着 Context 对象 ctx 和 query 调用 google.Search,代码如下:
如果搜索成功,处理函数会渲染搜索结果,代码如下:
###5.2 深入 userip 包
userip 包提供了两个功能:
从请求解析出Client IP;
将 Client IP 关联到一个
一个
为了避免 key 冲突,
函数
函数
函数
Google Web Search API 请求包含 query 关键字和 user IP 两个参数。具体实现如下:
函数
函数
将已有代码和期望 Context 参数的代码粘合起来。
举个栗子,Gorilla 框架的 github.com/gorilla/context 包允许处理函数 (handlers) 将数据和请求结合起来,他通过 HTTP 请求 到 key-value对 的映射来实现。在 gorilla.go 中,我们提供了一个 Context 的具体实现,这个实现的 Value 方法返回的值已经与 gorilla 包中特定的 HTTP 请求关联起来。
还有一些包实现了类似于 Context 的取消机制。比如 Tomb 中有一个 Kill 方法,该方法通过关闭 名为Dying 的 channel 发送取消信号。Tomb 也提供了等待 goroutine 退出的方法,类似于 sync.WaitGroup。在 tomb.go 中,我们提供了一个 Context 的实现,当它的父 Context 被取消
或 一个 Tomb 对象被 kill 时,该 Context 对象也会被取消。
如果你要在 Context 之上构建服务器框架,需要一个自己的 Context 实现,在框架与期望 Context 参数的代码之间建立一座桥梁。
当然,Client 库也需要接收一个 Context 对象。在请求作用域数据与取消之间建立了通用的接口以后,开发者使用 Context
分享代码、创建可扩展的服务都会非常方便。
原作者:Sameer Ajmani 翻译:Oscar
https://segmentfault.com/a/1190000006744213
https://studygolang.com/articles/9517
1 简介
在 Go http包的Server中,每一个请求在都有一个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和RPC服务。用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。在Google 内部,我们开发了 Context 包,专门用来简化 对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用。你可以通过 go get golang.org/x/net/context 命令获取这个包。本文要讲的就是如果使用这个包,同时也会提供一个完整的例子。
2 阅读建议
本文内容涉及到了 done channel,如果你不了解这个概念,那么请先阅读 “Go语言并发模型:像Unix Pipe那样使用channel”。Go语言并发模型:像Unix Pipe那样使用channel由于访问 golang.org/x/net/context 需要梯子,你可以访问它在 github 上的 mirror。
如果要下载本文中的代码,可以查看文章末尾的“相关链接”环节。
3 package context
context 包的核心是 struct Context,声明如下:// A Context carries a deadline, cancelation signal, and request-scoped values // across API boundaries. Its methods are safe for simultaneous use by multiple // goroutines. type Context interface { // Done returns a channel that is closed when this `Context` is canceled // or times out. Done() <-chan struct{} // Err indicates why this Context was canceled, after the Done channel // is closed. Err() error // Deadline returns the time when this Context will be canceled, if any. Deadline() (deadline time.Time, ok bool) // Value returns the value associated with key or nil if none. Value(key interface{}) interface{} }
注意: 这里我们对描述进行了简化,更详细的描述查看 godoc:context
Done方法返回一个 channel,这个 channel 对于以
Context方式运行的函数而言,是一个取消信号。当这个 channel 关闭时,上面提到的这些函数应该终止手头的工作并立即返回。 之后,
Err方法会返回一个错误,告知为什么
Context被取消。关于
Donechannel 的更多细节查看上一篇文章 “Go语言并发模型:像Unix Pipe那样使用channel”。
一个
Context不能拥有
Cancel方法,同时我们也只能
Donechannel 接收数据。背后的原因是一致的:接收取消信号的函数和发送信号的函数通常不是一个。 一个典型的场景是:父操作为子操作操作启动 goroutine,子操作也就不能取消父操作。 作为一个折中,
WithCancel函数 (后面会细说) 提供了一种取消新的
Context的方法。
Context对象是线程安全的,你可以把一个
Context对象传递给任意个数的 gorotuine,
对它执行 取消 操作时,所有 goroutine 都会接收到取消信号。
Deadline方法允许函数确定它们是否应该开始工作。如果剩下的时间太少,也许这些函数就不值得启动。代码中,我们也可以使用
Deadline对象为 I/O 操作设置截止时间。
Value方法允许
Context对象携带request作用域的数据,该数据必须是线程安全的。
4 继承 context
context 包提供了一些函数,协助用户从现有的Context对象创建新的
Context对象。
这些
Context对象形成一棵树:当一个
Context对象被取消时,继承自它的所有
Context都会被取消。
Background是所有
Context对象树的根,它不能被取消。它的声明如下:
// Background returns an empty Context. It is never canceled, has no deadline, // and has no values. Background is typically used in main, init, and tests, // and as the top-level `Context` for incoming requests. func Background() Context
WithCancel和
WithTimeout函数 会返回继承的
Context对象, 这些对象可以比它们的父
Context更早地取消。
当请求处理函数返回时,与该请求关联的
Context会被取消。 当使用多个副本发送请求时,可以使用
WithCancel取消多余的请求。
WithTimeout在设置对后端服务器请求截止时间时非常有用。 下面是这三个函数的声明:
// WithCancel returns a copy of parent whose Done channel is closed as soon as // parent.Done is closed or cancel is called. func WithCancel(parent Context) (ctx Context, cancel CancelFunc) // A CancelFunc cancels a Context. type CancelFunc func() // WithTimeout returns a copy of parent whose Done channel is closed as soon as // parent.Done is closed, cancel is called, or timeout elapses. The new // Context's Deadline is the sooner of now+timeout and the parent's deadline, if // any. If the timer is still running, the cancel function releases its // resources. func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithValue 函数能够将请求作用域的数据与 Context 对象建立关系。声明如下:
// WithValue returns a copy of parent whose Value method returns val for key. func WithValue(parent Context, key interface{}, val interface{}) Context
当然,想要知道 Context 包是如何工作的,最好的方法是看一个栗子。
5 一个栗子:Google Web Search
我们的例子是一个 HTTP 服务,它能够将类似于/search?q=golang&timeout=1s的请求 转发给
Google Web Search API,然后渲染返回的结果。
timeout参数用来告诉 server 时间到时取消请求。
这个例子的代码存放在三个包里:
server:它提供 main 函数和 处理
/search的 http handler
userip:它能够从 请求解析用户的IP,并将请求绑定到一个
Context对象。
google:它包含了 Search 函数,用来向 Google 发送请求。
###5.1 深入 server 程序
server 程序处理类似于 /search?q=golang 的请求,返回 Google API 的搜索结果。它将 handleSearch 函数注册到 /search 路由。处理函数创建一个 Context ctx,并对其进行初始化,以保证 Context 取消时,处理函数返回。如果请求的 URL 参数中包含 timeout,那么当 timeout 到期时, Context 会被自动取消。
handleSearch 的代码如下:
func handleSearch(w http.ResponseWriter, req *http.Request) { // ctx is the `Context` for this handler. Calling cancel closes the // ctx.Done channel, which is the cancellation signal for requests // started by this handler. var ( ctx context.Context cancel context.CancelFunc ) timeout, err := time.ParseDuration(req.FormValue("timeout")) if err == nil { // The request has a timeout, so create a `Context` that is // canceled automatically when the timeout expires. ctx, cancel = context.WithTimeout(context.Background(), timeout) } else { ctx, cancel = context.WithCancel(context.Background()) } defer cancel() // Cancel ctx as soon as handleSearch returns.
处理函数 (handleSearch) 将query 参数从请求中解析出来,然后通过 userip 包将client IP解析出来。这里 Client IP 在后端发送请求时要用到,所以 handleSearch 函数将它 attach 到 Context 对象 ctx 上。代码如下:
// Check the search query. query := req.FormValue("q") if query == "" { http.Error(w, "no query", http.StatusBadRequest) return } // Store the user IP in ctx for use by code in other packages. userIP, err := userip.FromRequest(req) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } ctx = userip.NewContext(ctx, userIP)
处理函数带着 Context 对象 ctx 和 query 调用 google.Search,代码如下:
// Run the Google search and print the results. start := time.Now() results, err := google.Search(ctx, query) elapsed := time.Since(start)
如果搜索成功,处理函数会渲染搜索结果,代码如下:
if err := resultsTemplate.Execute(w, struct { Results google.Results Timeout, Elapsed time.Duration }{ Results: results, Timeout: timeout, Elapsed: elapsed, }); err != nil { log.Print(err) return }
###5.2 深入 userip 包
userip 包提供了两个功能:
从请求解析出Client IP;
将 Client IP 关联到一个
Context对象。
一个
Context对象提供一个 key-value 映射,key 和 value的类型都是 interface{},但是 key 必须满足等价性(可以比较),value 必须是线程安全的。类似于
userip的包隐藏了映射的细节,提供的是对特定
Context类型值得强类型访问。
为了避免 key 冲突,
userip定义了一个非输出类型
key,并使用该类型的值作为
Context的key。代码如下:
// 为了避免与其他包中的 `Context` key 冲突 // 这里不输出 key 类型 (首字母小写) type key int // userIPKey 是 user IP 的 `Context` key // 它的值是随意写的。如果这个包中定义了其他 // `Context` key,这些 key 必须不同 const userIPKey key = 0
函数
FromRequest用来从一个 http.Request 对象中解析出 userIP:
func FromRequest(req *http.Request) (net.IP, error) { ip, _, err := net.SplitHostPort(req.RemoteAddr) if err != nil { return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr) }
函数
NewContext返回一个新的
Context对象,它携带者 userIP:
func NewContext(ctx context.Context, userIP net.IP) context.Context { return context.WithValue(ctx, userIPKey, userIP) }
函数
FromContext从一个
Context对象中解析 userIP:
func FromContext(ctx context.Context) (net.IP, bool) { // ctx.Value returns nil if ctx has no value for the key; // the net.IP type assertion returns ok=false for nil. userIP, ok := ctx.Value(userIPKey).(net.IP) return userIP, ok }
5.3 深入 google 包
函数google.Search想 Google Web Search API 发送一个 HTTP 请求,并解析返回的 JSON 数据。该函数接收一个
Context对象 ctx 作为第一参数,在请求还没有返回时,一旦
ctx.Done关闭,该函数也会立即返回。
Google Web Search API 请求包含 query 关键字和 user IP 两个参数。具体实现如下:
func Search(ctx context.Context, query string) (Results, error) { // Prepare the Google Search API request. req, err := http.NewRequest("GET", "https://ajax.googleapis.com/ajax/services/search/web?v=1.0", nil) if err != nil { return nil, err } q := req.URL.Query() q.Set("q", query) // If ctx is carrying the user IP address, forward it to the server. // Google APIs use the user IP to distinguish server-initiated requests // from end-user requests. if userIP, ok := userip.FromContext(ctx); ok { q.Set("userip", userIP.String()) } req.URL.RawQuery = q.Encode()
函数
Search使用一个辅助函数
httpDo发送 HTTP 请求,并在
ctx.Done关闭时取消请求 (如果还在处理请求或返回)。函数
Search传递给
httpDo一个闭包处理 HTTP 结果。下面是具体实现:
var results Results err = httpDo(ctx, req, func(resp *http.Response, err error) error { if err != nil { return err } defer resp.Body.Close() // Parse the JSON search result. // https://developers.google.com/web-search/docs/#fonje var data struct { ResponseData struct { Results []struct { TitleNoFormatting string URL string } } } if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { return err } for _, res := range data.ResponseData.Results { results = append(results, Result{Title: res.TitleNoFormatting, URL: res.URL}) } return nil }) // httpDo waits for the closure we provided to return, so it's safe to // read results here. return results, err
函数
httpDo在一个新的 goroutine 中发送 HTTP 请求和处理结果。如果
ctx.Done已经关闭,而处理请求的 goroutine 还存在,那么取消请求。下面是具体实现:
func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error { // Run the HTTP request in a goroutine and pass the response to f. tr := &http.Transport{} client := &http.Client{Transport: tr} c := make(chan error, 1) go func() { c <- f(client.Do(req)) }() select { case <-ctx.Done(): tr.CancelRequest(req) <-c // Wait for f to return. return ctx.Err() case err := <-c: return err } }
6 另一个栗子
package main import ( "fmt" "time" "golang.org/x/net/context" ) // 模拟一个最小执行时间的阻塞函数 func inc(a int) int { res := a + 1 // 虽然我只做了一次简单的 +1 的运算, time.Sleep(1 * time.Second) // 但是由于我的机器指令集中没有这条指令, // 所以在我执行了 1000000000 条机器指令, 续了 1s 之后, 我才终于得到结果。B) return res } // 向外部提供的阻塞接口 // 计算 a + b, 注意 a, b 均不能为负 // 如果计算被中断, 则返回 -1 func Add(ctx context.Context, a, b int) int { res := 0 for i := 0; i < a; i++ { res = inc(res) select { case <-ctx.Done(): return -1 default: } } for i := 0; i < b; i++ { res = inc(res) select { case <-ctx.Done(): return -1 default: } } return res } func main() { { // 使用开放的 API 计算 a+b a := 1 b := 2 timeout := 2 * time.Second ctx, _ := context.WithTimeout(context.Background(), timeout) res := Add(ctx, 1, 2) fmt.Printf("Compute: %d+%d, result: %d\n", a, b, res) } { // 手动取消 a := 1 b := 2 ctx, cancel := context.WithCancel(context.Background()) go func() { time.Sleep(2 * time.Second) cancel() // 在调用处主动取消 }() res := Add(ctx, 1, 2) fmt.Printf("Compute: %d+%d, result: %d\n", a, b, res) } }
7 在自己的代码中使用 Context
许多服务器框架都提供了管理请求作用域数据的包和类型。我们可以定义一个 Context 接口的实现,将已有代码和期望 Context 参数的代码粘合起来。
举个栗子,Gorilla 框架的 github.com/gorilla/context 包允许处理函数 (handlers) 将数据和请求结合起来,他通过 HTTP 请求 到 key-value对 的映射来实现。在 gorilla.go 中,我们提供了一个 Context 的具体实现,这个实现的 Value 方法返回的值已经与 gorilla 包中特定的 HTTP 请求关联起来。
还有一些包实现了类似于 Context 的取消机制。比如 Tomb 中有一个 Kill 方法,该方法通过关闭 名为Dying 的 channel 发送取消信号。Tomb 也提供了等待 goroutine 退出的方法,类似于 sync.WaitGroup。在 tomb.go 中,我们提供了一个 Context 的实现,当它的父 Context 被取消
或 一个 Tomb 对象被 kill 时,该 Context 对象也会被取消。
8 结论
在 Google, 我们要求 Go 程序员把 Context 作为第一个参数传递给 入口请求和出口请求链路上的每一个函数。这种机制一方面保证了多个团队开发的 Go 项目能够良好地协作,另一方面它是一种简单的超时和取消机制,保证了临界区数据 (比如安全凭证) 在不同的 Go 项目中顺利传递。如果你要在 Context 之上构建服务器框架,需要一个自己的 Context 实现,在框架与期望 Context 参数的代码之间建立一座桥梁。
当然,Client 库也需要接收一个 Context 对象。在请求作用域数据与取消之间建立了通用的接口以后,开发者使用 Context
分享代码、创建可扩展的服务都会非常方便。
原作者:Sameer Ajmani 翻译:Oscar
相关文章推荐
- go语言 grequests+goquery 简单爬虫,使用多协程并发爬取
- 在 Go 语言中,正确的使用并发
- golang实战使用gin+xorm搭建go语言web框架restgo详解5.5 控制器模型绑定
- Go语言并发模型的2种编程方案
- Go 语言的并发模型--通过通信来共享内存
- golang实战使用gin+xorm搭建go语言web框架restgo详解6.1 模型M和Orm
- 使用循环神经网络实现语言模型——源自《TensorFlow:实战Goole深度学习框架》
- Go语言并发与并行学习笔记(一)
- [阅读笔记]Go语言并发之美
- 28.笔记go语言——并发简单示例
- go语言接口使用
- 使用Sublime text 3打造一个小巧但强大的Go语言开发IDE
- Go语言结构体定义和使用方法
- Go语言中使用flag包对命令行进行参数解析的方法
- Go语言并发编程
- 五、建立语言模型几种方法及使用
- 在 go/golang语言中使用 google Protocol Buffer
- Go 语言并发笔记
- 使用Go语言画图,基础图
- Go语言中 循环的使用