Introduction
在Go服务中,每个传入请求在自己的goroutine中执行。请求处理程序通常启动额外的goroutine来访问后端,如数据库和RPC服务。处理请求的goroutine集合通常需要访问特定于请求的值,比如最终用户的身份,授权令牌和请求的截止日期。当一个请求被取消或超时时,处理该请求的所有goroutine都应该迅速退出,这样系统就可以回收它们正在使用的任何资源。
在谷歌,我们开发了一个上下文包,它可以很容易地将请求范围内的值、取消信号和截止日期跨API边界传递给处理请求所涉及的所有goroutine。该包作为上下文公开可用。本文描述了如何使用该包,并提供了一个完整的工作示例。
context
context包的核心是Context type:
// A Context carries a deadline, cancelation signal, and request-scoped values// across API boundaries. Its methods are safe for simultaneous use by multiple// goroutines.//Context带有截止日期、取消信号和请求范围的值//在API的界限。它的方法对于多人同时使用是安全的//了goroutine。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是权威的。)
Done方法返回一个通道,该通道充当代表上下文运行的函数的取消信号:当通道关闭时,函数应该放弃它们的工作并返回。Err方法返回一个错误,指出上下文被取消的原因。管道和取消文章更详细地讨论了完成通道习惯用法。
Context没有Cancel方法的原因与Done通道只接收的原因相同 : 接收抵消信号的函数通常不是发送信号的函数。特别是,当父操作启动子操作的goroutine时,这些子操作应该不能取消父操作。相反,WithCancel函数(如下所述)提供了一种取消新Context值的方法。
一个上下文是安全的,同时使用多个goroutine。代码可以将一个Context传递给任意数量的goroutine,并取消该Context来给所有的goroutine发出信号。
Deadline方法允许函数决定它们是否应该开始工作;如果剩下的时间太少,可能就不值得。代码也可以使用最后期限为I/O操作设置超时。
值允许Context携带请求范围的数据。这些数据必须是安全的,以便多个例行程序同时使用。
Derived contexts
上下文包提供了从现有的上下文值派生新的上下文值的函数。这些值形成一个树:当上下文被取消时,所有从它派生的上下文也被取消。
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.//Background返回一个空的Context。它从不被取消,没有截止日期,//没有值。Background通常用于main, init和tests,//并作为传入请求的顶级上下文。func Background() Context
WithCancel和WithTimeout返回派生的上下文值,可以比父上下文更快地被取消。当请求处理程序返回时,通常会取消与传入请求相关联的上下文。当使用多个副本时,WithCancel对于取消冗余请求也很有用。WithTimeout用于设置请求到后端服务器的最后期限:
// WithCancel returns a copy of parent whose Done channel is closed as soon as// parent.Done is closed or cancel is called.// WithCancel返回一个父类的副本,该父类的Done通道将立即关闭//的父类。Done是关闭的,cancel是调用的。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.//WithTimeout返回一个父类的副本,它的Done通道一旦关闭的父母。关闭Done,调用cancel,或超时。新上下文的截止日期是现在+超时和父的截止日期的较早,如果任何。如果计时器仍在运行,cancel函数将释放它的资源。func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithValue提供了一种方法将请求范围的值与Context关联起来:
// WithValue returns a copy of parent whose Value method returns val for key.// WithValue返回父类的副本,其Value方法返回val for key。func WithValue(parent Context, key interface{}, val interface{}) Context
了解如何使用上下文包的最佳方法是通过一个工作示例。
Example: Google Web Search
我们的例子是一个HTTP服务器,它处理像/search?q=golang&timeout=1s通过转发查询“golang”到谷歌Web搜索API并呈现结果。timeout参数告诉服务器在该时间过后取消请求。
代码被分为三个包:
- server provides themainfunction and the handler for/search.
- userip provides functions for extracting a user IP address from a request and associating it with aContext.
- google provides theSearchfunction for sending a query to Google.
The server program
服务器程序处理类似/search?q=golang通过提供golang的前几个谷歌搜索结果。它注册handleSearch来处理/搜索端点。该处理程序创建一个名为ctx的初始上下文,并安排在处理程序返回时取消它。如果请求包含timeout URL参数,超时后上下文自动取消:
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.Contextcancel 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.}
处理程序从请求中提取查询,并通过调用userip包提取客户机的IP地址。客户端的IP地址需要用于后端请求,所以handleSearch将其附加到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)
处理程序调用谷歌。使用ctx搜索和查询:
// 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.ResultsTimeout, Elapsed time.Duration}{Results: results,Timeout: timeout,Elapsed: elapsed,}); err != nil {log.Print(err)return}
Package userip
userip包提供了从请求中提取用户IP地址并将其与Context关联的功能。Context提供了一个键-值映射,其中键和值都是interface{}类型。键类型必须支持相等性,值必须是安全的,以便多个goroutine同时使用。像userip这样的包隐藏了这个映射的细节,并提供了对特定Context值的强类型访问。
为了避免键冲突,userip定义了一个未导出的类型键,并使用该类型的值作为上下文键 :
// The key type is unexported to prevent collisions with context keys defined in// other packages.//键类型未导出,以防止与定义的上下文键发生冲突其他包。type key int// userIPkey is the context key for the user IP address. Its value of zero is// arbitrary. If this package defined other context keys, they would have// different integer values.//userIPkey是用户IP地址的上下文键。它的0值是任意的。如果这个包定义了其他上下文键,它们就会有不同的整数值。const userIPKey key = 0
FromRequest从http中提取一个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返回一个带有提供的userIP值的新Context:
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}
Package google
google.Search 函数向谷歌Web Search API发出一个HTTP请求,并解析json编码的结果。它接受一个Context参数ctx,如果当请求处于飞行状态时,ctx.Done关闭,立即返回。
谷歌Web搜索API请求包括搜索查询和用户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取消请求时在处理请求或响应时关闭。搜索将一个闭包传递给httpDo来处理HTTP响应:
var results Resultserr = 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/#fonjevar data struct {ResponseData struct {Results []struct {TitleNoFormatting stringURL 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
