在Go中,每个请求都会在各自所在的goroutine中运行。Context包可以方便地在各个goroutine之间传值和发送取消[1]、达到*最后期限*[2]信号。
Context的接口定义
// Context携带着deadline和取消信号,和request-scoped的值跨域API的界限在goroutine之间传递,
// 且保证是同步安全的
type Context interface {
// 当此Context取消或者超时,Done返回一个channel
Done() <-chan struct{}
// 此context被取消的错误原因
Err() error
// Deadline返回什么时候Context由于超时会被取消
Deadline() (deadline time.Time, ok bool)
// request-scoped需要共享的值
Value(key interface{}) interface{}
}
获取Context
Context值是以树状结构呈现的,如果Context被取消,那么他的子Context也会被取消。
Background
是Context树的根,它不能被取消:
// Background返回一个空Context。它不允许被取消,没有最后期限,没有值。
// Background被用在main,init和tests的地方,作为进来的请求的最顶级Context。
func Background() Context
WithCancel
和WithTimeout
返回的Conetxt是可以被取消的。此Context关联的请求处理完成返回时,就会被取消。WithCancel
多用于关联冗余请求,WithTimeout
多用于关联后台服务要求设置超时的请求。
// WithCancel返回一个parent Context的副本,当parent的Done Channel被关闭,或者cancel被调用,
// 那么它的Done Channel也会被关闭
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// A CancelFunc cancels a Context.
type CancelFunc func()
// WithTimeout返回一个parent Context的副本,当parent的Done Channel被关闭,或者cancel被调用,
// 或者超时时间已到,那么它的Done Channel也会被关闭。
// 此新Context的最后期限必须比now + timeout要早。如果timer仍然在运行,那么cancel方法会释放资源。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithValue
提供了一个关联request-scoped值的方法。
// WithValue返回一个Parent的副本,它的Value方法返回key匹配的val值
func WithValue(parent Context, key interface{}, val interface{}) Context
下面提供一个最佳实践。
例子:Google Web Search
我们的例子是处理一个http请求,URL是/search?q=golang&timeout=1s
表示查询golang
关键字,timeout
参数表示请求的超时时间,最后调用Google Web Search API获取数据并渲染结果。
代码分成三个包:
- server提供main方法和处理
/search
请求 - userip提供抽取IP地址,并把它关联到Context的方法
- google提供
Search
方法,调用google的api
server代码
此处理器创建第一个Context,称为ctx;同时把它设置为当处理返回后被取消。如果URL包含timeout参数,此Context超时后也会被自动取消。
func handleSearch(w http.ResponseWriter, req *http.Request) {
// ctx是此处理器的Context。调用cancel方法会关闭ctx.Done channel,此取消信号是此处理器发出
var (
ctx context.Context
cancel context.CancelFunc
)
timeout, err := time.ParseDuration(req.FormValue("timeout"))
if err == nil {
// 此请求有超时入参,所以此context超时后会被自动取消
ctx, cancel = context.WithTimeout(context.Background(), timeout)
} else {
ctx, cancel = context.WithCancel(context.Background())
}
// handleSearch返回后发送取消信号,取消ctx
defer cancel()
此处理器在请求里抽取出客户端IP地址,然后调用userip
包的方法。此客户端IP地址在后面的请求中会用到,所以handleSearch把它附在ctx里:
// 获取入参q的值并校验
query := req.FormValue("q")
if query == "" {
http.Error(w, "no query", http.StatusBadRequest)
return
}
// 使用其他包的代码存储客户端IP地址
userIP, err := userip.FromRequest(req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
ctx = userip.NewContext(ctx, userIP)
此处理器调用google.Search
方法,并带上了ctx
和query
。
// 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
包提供从请求抽取IP地址和把IP地址关联到Context的功能。Context提供key-value映射关系存储。
为了避免key发生碰撞,userip
定义一个私有类型key,使用此类型的值作为Context的key:
// 此key类型是私有的,避免与其他包所定义的key发生碰撞
type key int
// userIPkey是客户端IP地址的Context key。如果此包定义了其他的Context key,他们需要使用其他整数值
const userIPKey key = 0
FromRequest
从http.Request
抽取出客户端IP地址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) {
// 如果不存在此key对应的value,ctx.Value返回nil
userIP, ok := ctx.Value(userIPKey).(net.IP)
return userIP, ok
}
google包
google.Search 方法创建一个HTTP请求 Google Web Search API 接口,并把结果解析成JSON结构。它接受一个Context类型的参数ctx,并且当请求没有响应导致 ctx.Done
被关闭时会马上返回。
func Search(ctx context.Context, query string) (Results, error) {
// 准备Google Search API请求.
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()
// 参数1
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 {
// 参数2
q.Set("userip", userIP.String())
}
req.URL.RawQuery = q.Encode()
Search
使用了一个辅助方法, httpDo
,发起HTTP请求,如果 ctx.Done
在处理请求或响应时关闭了HTTP请求,则将其取消。Search
传递了一个闭包方法来处理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 {
// 在一个goroutine中运行一个HTTP请求,并且把处理响应的逻辑方法传递到f入参
c := make(chan error, 1)
req = req.WithContext(ctx)
go func() { c <- f(http.DefaultClient.Do(req)) }()
select {
case <-ctx.Done():
<-c // Wait for f to return.
return ctx.Err()
case err := <-c:
return err
}
}
总结
在Google,我们要求Go程序员必须把Context
作为第一入参传递到每个有传入传出的请求函数中。这样可以使不同的Go开发团队很好地进行互动操作。他提供对超时和取消的简单控制,并保证安全凭证之类的关键值正确地传导到Go程序。
希望基于Context
开发的服务框架需要提供实现,让你的包和那些需要一个Context
参数的包建立桥梁联系。客户端库会接受一个来自调用方的Context
。通过为request-scoped和取消建立一个通用接口,Context
可以让包开发者更容易地为可伸缩服务分享代码。