原文地址: Go Concurrency Patterns: Context
Introduction
在Go server中,新的請求通常都會起一個新的goroutine處理,這個goroutine又通常會起一些額外的goroutines來訪問後端,例如database,RPC服務等。這一系列處理一個請求的goroutines通常需要訪問請求相關的數據,例如最終用戶的身份,Authorization tokens,請求截止時間等。當一個請求退出或者超時,所有爲這個請求工作的goroutines都必須立即退出,然後系統才能回收他們佔用的資源。
Google開發並開源了一個context
包可以很容易的通過API邊界向所有處理同一個請求的goroutines傳遞請求範圍的數據,退出信號,截止時間等(goroutines之間的全局變量共享, 以及同步退出控制)。這篇文章描述瞭如何使用這個包,以及提供一個完整的可工作的示例。
Context
context包的核心就是定義的Context
interface。
// 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{}
}
Done
方法返回一個作爲退出信號的channel到運行在Context中的函數(通俗的講就是接受Context變量的Goroutines通過Done方法獲取退出信號)。如果channel被關閉(close),該函數(goroutines)應當立即停止工作並退出。Pipelines and Cancelation這篇文章更詳細的討論了Done
channel。
Error
方法返回一個error用於指示Context
退出的理由。
Context
沒有一個Cancel
方法,原因同Done
返回的channel是一個只能用於從channel讀取數據的單向channel一樣:就是接收退出信號的函數通常不是信號的發送方。通常情況下,當一個父親起了一些goroutines處理一些子任務,這些子goroutines是不能終止parent並讓其退出的。作爲替代下面將提到的WithCancel
方法將提供一個方法用於退出一個新的Context值。
Context對被多個goroutines同時使用是安全的。代碼可以將同一個Context傳給任何數量的goroutines使用,並像它們發送信號指示退出這個context。
Deadline
方法允許函數決定是否啓動工作;如果距離timeout只有很少的時間,可能就沒有必要啓動執行了。代碼還可以使用Deadline
爲IO操作設置超時。
Value
方法允許Context攜帶一些請求範圍的數據。這些數據必須是對多個goroutines同時訪問安全的。
Derived 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
返回一個可以比parent Context更早被退出的派生Context。典型情況下,與請求相關的Context通常在請求Handler退出時退出。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
Example: Google Web Search
我們的示例是一個HTTP server,它處理像/search?q=golang&timeout=1s
一樣的URLs,轉發查詢golang
的請求到Google Web Search API並翻譯結果。timeout
告訴服務器多長時間後退出請求。
代碼被分成了三個部分:
- server提供了
main
函數以及/search
的handler; - userip提供了從請求中解析用戶IP並將它關聯的
Context
的功能; - google提供了
Search
函數,用於向Google發送查詢請求。
server部分代碼
server
代碼通過爲golang提供Google的前幾個搜索結果來處理像/search?q=golang
一樣的請求。它爲註冊了一個handleSearch
來處理/search
API。這個handler創建了一個叫做ctx的初始Context
,並在handler返回時安排ctx
退出。如果請求中還包含了叫timeout
的URL參數,ctx將在超時時自動退出。
handler解析請求中的查詢條件,還會通過調用userip
包來解析請求中的用戶IP。用戶IP信息在後端服務中被需要,所以handleSearch
將它存入ctx
中。
// The server program issues Google search requests and demonstrates the use of
// the go.net Context API. It serves on port 8080.
//
// The /search endpoint accepts these query params:
// q=the Google search query
// timeout=a timeout for the request, in time.Duration format
//
// For example, http://localhost:8080/search?q=golang&timeout=1s serves the
// first few Google search results for "golang" or a "deadline exceeded" error
// if the timeout expires.
package main
import (
"html/template"
"log"
"net/http"
"time"
"golang.org/x/blog/content/context/google"
"golang.org/x/blog/content/context/userip"
"golang.org/x/net/context"
)
func main() {
http.HandleFunc("/search", handleSearch)
log.Fatal(http.ListenAndServe(":8080", nil))
}
// handleSearch handles URLs like /search?q=golang&timeout=1s by forwarding the
// query to google.Search. If the query param includes timeout, the search is
// canceled after that duration elapses.
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.
// 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)
// Run the Google search and print the results.
start := time.Now()
results, err := google.Search(ctx, query)
elapsed := time.Since(start)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
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
}
}
var resultsTemplate = template.Must(template.New("results").Parse(`
<html>
<head/>
<body>
<ol>
{{range .Results}}
<li>{{.Title}} - <a href="{{.URL}}">{{.URL}}</a></li>
{{end}}
</ol>
<p>{{len .Results}} results in {{.Elapsed}}; timeout {{.Timeout}}</p>
</body>
</html>
`))
Package userip
userip
包提供了從用戶請求中解析用戶IP,並將它關聯到Context的功能。一個Context提供了一個key-value mapping,其中key和value的類型都是interface{}。key類型必須支持等(equal)判斷,並且value必須對多個goroutines同時訪問安全。像userip
這樣的軟件包應該隱藏所有的映射細節,並提供對指定Context
value的強類型訪問。
爲了避免key衝突,userip
定義了一個不對外暴露的key
類型,並使用這個類型的數據作爲Context的key。
// Package userip provides functions for extracting a user IP address from a
// request and associating it with a Context.
//
// This package is an example to accompany https://blog.golang.org/context.
// It is not intended for use by others.
package userip
import (
"fmt"
"net"
"net/http"
"golang.org/x/net/context"
)
// FromRequest extracts the user IP address from req, if present.
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)
}
userIP := net.ParseIP(ip)
if userIP == nil {
return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
}
return userIP, nil
}
// 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.
const userIPKey key = 0
// NewContext returns a new Context carrying userIP.
func NewContext(ctx context.Context, userIP net.IP) context.Context {
return context.WithValue(ctx, userIPKey, userIP)
}
// FromContext extracts the user IP address from ctx, if present.
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
goole.Search
函數會向Google Web Search API發送一個HTTP請求,並解析返回的JSON編碼的結果。它接收一個Context類型的參數ctx
並且在request還沒有執行完成,但是ctx.Done
被關閉的時候立即返回。
Google Web Search API要求提供查詢條件和用戶IP做爲參數。
Search
使用了一個幫助函數httpDo
,由httpDo
發起HTTP請求。如果在request或response仍然在處理時,ctx.Done
被關閉時,httpDo
也將被退出。Search
傳遞了一個閉包給httpDo
來處理HTTP response。
// Package google provides a function to do Google searches using the Google Web
// Search API. See https://developers.google.com/web-search/docs/
//
// This package is an example to accompany https://blog.golang.org/context.
// It is not intended for use by others.
//
// Google has since disabled its search API,
// and so this package is no longer useful.
package google
import (
"encoding/json"
"net/http"
"golang.org/x/blog/content/context/userip"
"golang.org/x/net/context"
)
// Results is an ordered list of search results.
type Results []Result
// A Result contains the title and URL of a search result.
type Result struct {
Title, URL string
}
// Search sends query to Google search and returns the results.
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()
// Issue the HTTP request and handle the response. The httpDo function
// cancels the request if ctx.Done is closed.
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 issues the HTTP request and calls f with the response. If ctx.Done is
// closed while the request or f is running, httpDo cancels the request, waits
// for f to exit, and returns ctx.Err. Otherwise, httpDo returns f's error.
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
}
}
Adapting code for Contexts
有些服務框架提供包或類型用來攜帶請求範圍的參數。我們可以定義一個新的Context interface實現來作爲使用框架的代碼和希望使用Context作爲參數的代碼之間的橋樑。
例如,Gorilla的github.com/gorilla/context包允許handler通過提供將HTTP請求到key-value的映射來關聯請求和對應的數據。在下面的gorilla.go
代碼中,我們提供了一個Context實現,它的Value
方法返回了Gorilla包中指定HTTP request的數據。
// +build OMIT
// Package gorilla provides a go.net/context.Context implementation whose Value
// method returns the values associated with a specific HTTP request in the
// github.com/gorilla/context package.
package gorilla
import (
"net/http"
gcontext "github.com/gorilla/context"
"golang.org/x/net/context"
)
// NewContext returns a Context whose Value method returns values associated
// with req using the Gorilla context package:
// http://www.gorillatoolkit.org/pkg/context
func NewContext(parent context.Context, req *http.Request) context.Context {
return &wrapper{parent, req}
}
type wrapper struct {
context.Context
req *http.Request
}
type key int
const reqKey key = 0
// Value returns Gorilla's context package's value for this Context's request
// and key. It delegates to the parent Context if there is no such value.
func (ctx *wrapper) Value(key interface{}) interface{} {
if key == reqKey {
return ctx.req
}
if val, ok := gcontext.GetOk(ctx.req, key); ok {
return val
}
return ctx.Context.Value(key)
}
// HTTPRequest returns the *http.Request associated with ctx using NewContext,
// if any.
func HTTPRequest(ctx context.Context) (*http.Request, bool) {
// We cannot use ctx.(*wrapper).req to get the request because ctx may
// be a Context derived from a *wrapper. Instead, we use Value to
// access the request if it is anywhere up the Context tree.
req, ok := ctx.Value(reqKey).(*http.Request)
return req, ok
}
其他的一些包可能提供了類似Context的退出機制。例如Tomb提供了一個Kill
方法來發起通過關閉一個Dying channel來退出。Tomb還提供了方法等待所有的goroutines退出,類似於sync.WaitGroup
。在下面的tomb.go
中我們實現了一個Context,它將在父Context退出,或提供的Tomb被kill時退出。
// +build OMIT
// Package tomb provides a Context implementation that is canceled when either
// its parent Context is canceled or a provided Tomb is killed.
package tomb
import (
"golang.org/x/net/context"
tomb "gopkg.in/tomb.v2"
)
// NewContext returns a Context that is canceled either when parent is canceled
// or when t is Killed.
func NewContext(parent context.Context, t *tomb.Tomb) context.Context {
ctx, cancel := context.WithCancel(parent)
go func() {
select {
case <-t.Dying():
cancel()
case <-ctx.Done():
}
}()
return ctx
}
結論
在Google所有的Go程序員都被要求將Context變量作爲傳入和傳出請求之間所有調用路徑上的函數的第一個參數。這允許不同團隊之間開發的Go代碼能夠良好的交互。它提供了簡單的超時和退出機制,並確保諸如安全證書之類的數據能夠在Go程序間正確的傳遞。
如果想基於Context使用一些服務框架,則需要提供一個Context實現作爲包和期望的Context參數之間的橋樑。這樣它們的client就可以通過這個橋樑來接收一個Context。通過爲請求範圍的數據和退出機制建立通用的接口,Context使得包開發者能夠爲創建可擴展的服務更容易共享代碼。