Introduction

在Go服务中,每个传入请求在自己的goroutine中执行。请求处理程序通常启动额外的goroutine来访问后端,如数据库和RPC服务。处理请求的goroutine集合通常需要访问特定于请求的值,比如最终用户的身份,授权令牌和请求的截止日期。当一个请求被取消或超时时,处理该请求的所有goroutine都应该迅速退出,这样系统就可以回收它们正在使用的任何资源。

在谷歌,我们开发了一个上下文包,它可以很容易地将请求范围内的值、取消信号和截止日期跨API边界传递给处理请求所涉及的所有goroutine。该包作为上下文公开可用。本文描述了如何使用该包,并提供了一个完整的工作示例。

context

context包的核心是Context type:

  1. // A Context carries a deadline, cancelation signal, and request-scoped values
  2. // across API boundaries. Its methods are safe for simultaneous use by multiple
  3. // goroutines.
  4. //Context带有截止日期、取消信号和请求范围的值
  5. //在API的界限。它的方法对于多人同时使用是安全的
  6. //了goroutine。
  7. type Context interface {
  8. // Done returns a channel that is closed when this Context is canceled
  9. // or times out.
  10. Done() <-chan struct{}
  11. // Err indicates why this context was canceled, after the Done channel
  12. // is closed.
  13. Err() error
  14. // Deadline returns the time when this Context will be canceled, if any.
  15. Deadline() (deadline time.Time, ok bool)
  16. // Value returns the value associated with key or nil if none.
  17. Value(key interface{}) interface{}
  18. }

(这个描述是浓缩的;godoc是权威的。)

Done方法返回一个通道,该通道充当代表上下文运行的函数的取消信号:当通道关闭时,函数应该放弃它们的工作并返回。Err方法返回一个错误,指出上下文被取消的原因。管道和取消文章更详细地讨论了完成通道习惯用法。

Context没有Cancel方法的原因与Done通道只接收的原因相同 : 接收抵消信号的函数通常不是发送信号的函数。特别是,当父操作启动子操作的goroutine时,这些子操作应该不能取消父操作。相反,WithCancel函数(如下所述)提供了一种取消新Context值的方法。

一个上下文是安全的,同时使用多个goroutine。代码可以将一个Context传递给任意数量的goroutine,并取消该Context来给所有的goroutine发出信号。

Deadline方法允许函数决定它们是否应该开始工作;如果剩下的时间太少,可能就不值得。代码也可以使用最后期限为I/O操作设置超时。

值允许Context携带请求范围的数据。这些数据必须是安全的,以便多个例行程序同时使用。

Derived contexts

上下文包提供了从现有的上下文值派生新的上下文值的函数。这些值形成一个树:当上下文被取消时,所有从它派生的上下文也被取消。

Background是任何Context树的根;它永远不会被取消:

  1. // Background returns an empty Context. It is never canceled, has no deadline,
  2. // and has no values. Background is typically used in main, init, and tests,
  3. // and as the top-level Context for incoming requests.
  4. //Background返回一个空的Context。它从不被取消,没有截止日期,
  5. //没有值。Background通常用于main, init和tests,
  6. //并作为传入请求的顶级上下文。
  7. func Background() Context

WithCancel和WithTimeout返回派生的上下文值,可以比父上下文更快地被取消。当请求处理程序返回时,通常会取消与传入请求相关联的上下文。当使用多个副本时,WithCancel对于取消冗余请求也很有用。WithTimeout用于设置请求到后端服务器的最后期限:

  1. // WithCancel returns a copy of parent whose Done channel is closed as soon as
  2. // parent.Done is closed or cancel is called.
  3. // WithCancel返回一个父类的副本,该父类的Done通道将立即关闭
  4. //的父类。Done是关闭的,cancel是调用的。
  5. func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
  6. // A CancelFunc cancels a Context.
  7. type CancelFunc func()
  8. // WithTimeout returns a copy of parent whose Done channel is closed as soon as
  9. // parent.Done is closed, cancel is called, or timeout elapses. The new
  10. // Context's Deadline is the sooner of now+timeout and the parent's deadline, if
  11. // any. If the timer is still running, the cancel function releases its
  12. // resources.
  13. //WithTimeout返回一个父类的副本,它的Done通道一旦关闭
  14. 的父母。关闭Done,调用cancel,或超时。新
  15. 上下文的截止日期是现在+超时和父的截止日期的较早,如果
  16. 任何。如果计时器仍在运行,cancel函数将释放它的
  17. 资源。
  18. func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithValue提供了一种方法将请求范围的值与Context关联起来:

  1. // WithValue returns a copy of parent whose Value method returns val for key.
  2. // WithValue返回父类的副本,其Value方法返回val for key。
  3. 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参数,超时后上下文自动取消:

  1. func handleSearch(w http.ResponseWriter, req *http.Request) {
  2. // ctx is the Context for this handler. Calling cancel closes the
  3. // ctx.Done channel, which is the cancellation signal for requests
  4. // started by this handler.
  5. var (
  6. ctx context.Context
  7. cancel context.CancelFunc
  8. )
  9. timeout, err := time.ParseDuration(req.FormValue("timeout"))
  10. if err == nil {
  11. // The request has a timeout, so create a context that is
  12. // canceled automatically when the timeout expires.
  13. ctx, cancel = context.WithTimeout(context.Background(), timeout)
  14. } else {
  15. ctx, cancel = context.WithCancel(context.Background())
  16. }
  17. defer cancel() // Cancel ctx as soon as handleSearch returns.
  18. }

处理程序从请求中提取查询,并通过调用userip包提取客户机的IP地址。客户端的IP地址需要用于后端请求,所以handleSearch将其附加到ctx:

  1. // Check the search query.
  2. query := req.FormValue("q")
  3. if query == "" {
  4. http.Error(w, "no query", http.StatusBadRequest)
  5. return
  6. }
  7. // Store the user IP in ctx for use by code in other packages.
  8. userIP, err := userip.FromRequest(req)
  9. if err != nil {
  10. http.Error(w, err.Error(), http.StatusBadRequest)
  11. return
  12. }
  13. ctx = userip.NewContext(ctx, userIP)

处理程序调用谷歌。使用ctx搜索和查询:

  1. // Run the Google search and print the results.
  2. start := time.Now()
  3. results, err := google.Search(ctx, query)
  4. elapsed := time.Since(start)

如果搜索成功,处理程序将显示结果:

  1. if err := resultsTemplate.Execute(w, struct {
  2. Results google.Results
  3. Timeout, Elapsed time.Duration
  4. }{
  5. Results: results,
  6. Timeout: timeout,
  7. Elapsed: elapsed,
  8. }); err != nil {
  9. log.Print(err)
  10. return
  11. }

Package userip

userip包提供了从请求中提取用户IP地址并将其与Context关联的功能。Context提供了一个键-值映射,其中键和值都是interface{}类型。键类型必须支持相等性,值必须是安全的,以便多个goroutine同时使用。像userip这样的包隐藏了这个映射的细节,并提供了对特定Context值的强类型访问。

为了避免键冲突,userip定义了一个未导出的类型键,并使用该类型的值作为上下文键 :

  1. // The key type is unexported to prevent collisions with context keys defined in
  2. // other packages.
  3. //键类型未导出,以防止与定义的上下文键发生冲突其他包。
  4. type key int
  5. // userIPkey is the context key for the user IP address. Its value of zero is
  6. // arbitrary. If this package defined other context keys, they would have
  7. // different integer values.
  8. //userIPkey是用户IP地址的上下文键。它的0值是任意的。如果这个包定义了其他上下文键,它们就会有不同的整数值。
  9. const userIPKey key = 0

FromRequest从http中提取一个userIP值。要求:

  1. func FromRequest(req *http.Request) (net.IP, error) {
  2. ip, _, err := net.SplitHostPort(req.RemoteAddr)
  3. if err != nil {
  4. return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
  5. }

NewContext返回一个带有提供的userIP值的新Context:

  1. func NewContext(ctx context.Context, userIP net.IP) context.Context {
  2. return context.WithValue(ctx, userIPKey, userIP)
  3. }

FromContext从Context中提取一个userIP:

  1. func FromContext(ctx context.Context) (net.IP, bool) {
  2. // ctx.Value returns nil if ctx has no value for the key;
  3. // the net.IP type assertion returns ok=false for nil.
  4. userIP, ok := ctx.Value(userIPKey).(net.IP)
  5. return userIP, ok
  6. }

Package google

google.Search 函数向谷歌Web Search API发出一个HTTP请求,并解析json编码的结果。它接受一个Context参数ctx,如果当请求处于飞行状态时,ctx.Done关闭,立即返回。

谷歌Web搜索API请求包括搜索查询和用户IP作为查询参数:

  1. func Search(ctx context.Context, query string) (Results, error) {
  2. // Prepare the Google Search API request.
  3. req, err := http.NewRequest("GET", "https://ajax.googleapis.com/ajax/services/search/web?v=1.0", nil)
  4. if err != nil {
  5. return nil, err
  6. }
  7. q := req.URL.Query()
  8. q.Set("q", query)
  9. // If ctx is carrying the user IP address, forward it to the server.
  10. // Google APIs use the user IP to distinguish server-initiated requests
  11. // from end-user requests.
  12. if userIP, ok := userip.FromContext(ctx); ok {
  13. q.Set("userip", userIP.String())
  14. }
  15. req.URL.RawQuery = q.Encode()

Search 使用一个助手函数httpDo来发出HTTP请求并在ctx.Done取消请求时在处理请求或响应时关闭。搜索将一个闭包传递给httpDo来处理HTTP响应:

  1. var results Results
  2. err = httpDo(ctx, req, func(resp *http.Response, err error) error {
  3. if err != nil {
  4. return err
  5. }
  6. defer resp.Body.Close()
  7. // Parse the JSON search result.
  8. // https://developers.google.com/web-search/docs/#fonje
  9. var data struct {
  10. ResponseData struct {
  11. Results []struct {
  12. TitleNoFormatting string
  13. URL string
  14. }
  15. }
  16. }
  17. if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
  18. return err
  19. }
  20. for _, res := range data.ResponseData.Results {
  21. results = append(results, Result{Title: res.TitleNoFormatting, URL: res.URL})
  22. }
  23. return nil
  24. })
  25. // httpDo waits for the closure we provided to return, so it's safe to
  26. // read results here.
  27. return results, err