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.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.
}
处理程序从请求中提取查询,并通过调用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.Results
Timeout, 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 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