Introduction
In the Server of the Go http package, each request has a corresponding goroutine to process. Request handlers usually start additional goroutines to access backend services, such as databases and RPC services. The goroutine used to process a request usually needs to access some request-specific data, such as end-user authentication information, authentication-related tokens, and request deadlines. When a request is canceled or times out, all goroutines used to process the request should exit quickly before the system can release the resources occupied by these goroutines.
Internally at Google, we have developed Context
packages specifically to simplify operations related to request domain data, cancellation signals, deadlines, etc. between multiple goroutines handling a single request, which may involve multiple API calls. You can go get golang.org/x/net/context
get this package by command. What this article is about is if you use this package, and also provides a complete example.
reading suggestions
This article deals with done channels. If you don't understand this concept, please read "The Concurrency Model in Go: Using Channels Like Unix Pipes" .
Since access requires a ladder, you can access it mirrorgolang.org/x/net/context
on github . To download the code in this article, check out the "Related Links" section at the end of the article.
package context
The core of the context package is struct Context, which is declared as follows:
// 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{} }
Note: We have simplified the description here, for a more detailed description see godoc:context
Done
The method returns a channel, which Context
is a cancellation signal to functions running as mode. When the channel is closed, the functions mentioned above should terminate the work at hand and return immediately. After that, the Err
method returns an error telling why it Context
was canceled. For more details on Done
channels see the previous article "The Concurrency Model in Go: Using Channels Like Unix Pipes" .
One Context
cannot have Cancel
methods, and we can only Done
receive data on channels. The reasoning behind is the same: the function that receives the cancel signal and the function that sends the signal are usually not the same. A typical scenario is: a parent operation starts a goroutine for a child operation, and the child operation cannot cancel the parent operation. As a compromise, WithCancel
functions (more on this later) provide a way to cancel new ones Context
.
Context
Objects are thread-safe, you can pass an Context
object to any number of gorotuines, and
when you cancel it, all goroutines will receive the cancel signal.
Deadline
methods allow functions to determine if they should start working. If there is too little time left, maybe these functions are not worth starting. In code, we can also use Deadline
objects to set deadlines for I/O operations.
Value
Methods allow Context
objects to carry request-scoped data, which must be thread-safe.
inherit 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
包是如何工作的,最好的方法是看一个栗子。
一个栗子:Google Web Search
我们的例子是一个 HTTP 服务,它能够将类似于 /search?q=golang&timeout=1s
的请求 转发给
Google Web Search API,然后渲染返回的结果。timeout
参数用来告诉 server 时间到时取消请求。
这个例子的代码存放在三个包里:
server:它提供 main 函数和 处理
/search
的 http handleruserip:它能够从 请求解析用户的IP,并将请求绑定到一个
Context
对象。google:它包含了 Search 函数,用来向 Google 发送请求。
深入 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 }
深入 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 }
深入 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 } }
在自己的代码中使用 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
对象也会被取消。
结论
在 Google, 我们要求 Go 程序员把 Context
作为第一个参数传递给 入口请求和出口请求链路上的每一个函数。这种机制一方面保证了多个团队开发的 Go 项目能够良好地协作,另一方面它是一种简单的超时和取消机制,保证了临界区数据 (比如安全凭证) 在不同的 Go 项目中顺利传递。
如果你要在 Context
之上构建服务器框架,需要一个自己的 Context
实现,在框架与期望 Context
参数的代码之间建立一座桥梁。
当然,Client 库也需要接收一个 Context
对象。在请求作用域数据与取消之间建立了通用的接口以后,开发者使用 Context
分享代码、创建可扩展的服务都会非常方便。
原作者:Sameer Ajmani 翻译:Oscar
下期预告:Go语言并发模型:使用 select (原文链接)。