做者:Sameer Ajmani | 地址:blog.golang.org/contextgit
第二篇官方博客的翻譯,主要是關於 Go 併發控制的 context 包。github
整體來講,我認爲上一篇纔是 Go 併發的基礎與核心。context 是在前章基礎之上,爲 goroutine 控制而開發的一套便於使用的庫。畢竟,在不一樣的 goroutine 之間只傳遞 done channel,包含信息量確實是太少。golang
文章簡單介紹了 context 提供的方法,以及簡單介紹它們如何使用。接着,經過一個搜索的例子,介紹了在真實場景下的使用。web
文章的尾部部分說明了,除了官方實現的 context,也有一些第三方的實現,好比 github.com/context 和 Tomb,但這些在官方 context 出現以後就已經中止更新了。其實緣由很簡單,畢竟通常都是官方更強大。以前,go 模塊管理也是百花齊放,但最近官方推出本身的解決方案,或許不久,其餘方式都將會淘汰。ajax
其實,我以爲這篇文章並很差讀,感受不夠按部就班。忽然的一個例子或許會讓人有點懵逼。數據庫
翻譯正文以下:json
Go 的服務中,每一個請求都會有獨立的 goroutine 處理,每一個 goroutine 一般會啓動新的 goroutine 執行一些額外的工做,好比進行數據庫或 RPC 服務的訪問。同請求內的 goroutine 需能共享請求數據訪問,好比,用戶認證,受權 token,以及請求截止時間。若是請求取消或發生超時,請求範圍內的全部 goroutine 都應馬上退出,進行資源回收.後端
在 Google,咱們開發了一個 context 的包,經過它,咱們能夠很是方便地在請求內的 goroutine 之間傳遞請求數據、取消信號和超時信息。詳情查看 context。api
本文將會具體介紹 context 包的使用,並提供一個完整的使用案例。安全
context 的核心是 Context 類型。定義以下:
// A Context carries a deadline,cancellation signal,and request-scoped values
// across API. Its methods are safe for simultaneous use by multiple goroutines
// 一個 Context 能夠在 API (不管是不是協程間) 之間傳遞截止日期、取消信號、請求數據。
// Context 中的方法都是協程安全的。
type Context interface {
// Done returns a channel that is closed when this context is cancelled
// or times out.
// Done 方法返回一個 channel,當 context 取消或超時,Done 將關閉。
Done() <-chan struct{}
// Err indicates why this context was canceled, after the Done channel
// is closed
// 在 Done 關閉後,Err 可用於代表 context 被取消的緣由
Err() error
// Deadline returns the time when this Context will be canceled, if any.
// 到期則取消 context
Deadline() (deadline time.Time, ok bool)
// Value returns the value associated with key or nil if none
Value(key interface{}) interface{}
}
複製代碼
介紹比較簡要,詳細信息查看 godoc。
Done 方法返回的是一個 channel,它可用於接收 context 的取消信號。當 channel 關閉,監聽 Done 信號的函數會馬上放棄當前正在執行的工做並返回。Err 方法返回一個 error 變量,從它之中能夠知道 context 爲何被取消。pipeline and cancelation 一文對 Done channel 做了詳細介紹。
爲何 Context 沒有 cancel 方法,它的緣由與 Done channel 只讀的緣由相似,即接收取消信號的 goroutine 經過不會負責取消信號的發出。特別是,當父級啓動子級 goroutine 來執行操做,子級是沒法取消父級的。反之,WithCancel 方法(接下來介紹)提供了一種方式取消新建立的 Context。
Context 是協程併發安全的。咱們能夠將 Context 傳遞給任意數量的 goroutine,經過 cancel 能夠給全部的 goroutine 發送信號。
Deadline 方法可讓函數決定是否須要啓動工做,若是剩餘時間過短,那麼啓動工做就不值得了。在代碼中,咱們能夠經過 deadline 爲 IO 操做設置超時時間。
Value 方法可讓 context 在 goroutine 之間共享請求範圍內的數據,這些數據須要是協程併發安全的。
context 包提供了多個函數從已有的 Context 實例派生新的 Context。這些 Context 將會造成一個樹狀結構,只要一個 Context 取消,派生的 context 將都被取消。
Background 函數返回的 Context 是任何 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 會派生出新的 Context 實例,派生實例比父級更早被取消。與請求關聯的 Context 實例,在請求處理完成後將被取消。當遇到多副本的數據請求時,WithCancel 可用於取消多餘請求。在請求後端服務時,WithTimeout 可用於設置超時時間。
// WithCancel returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed or cancel is called.
// WithCanal 返回父級 Context 副本,當父級的 Done channel 關閉或調用 cancel,它的 Done channel 也會關閉。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) // A CancelFunc cancels a Context. // CancelFunc 用於取消 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. // 返回父級 Context 副本和 CancelFunc,三種狀況,它的 Done 會關閉,分別是父級 Done 關閉,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.
func WithValue(parent Context, key interface{}, val interface{}) Context 複製代碼
context 如何使用呢?最好的方式,經過一個案例演示。
演示一個案例,實現一個 HTTP 服務,處理相似 /search?q=golang&timeout=1s 的請求。timeout 表示若是請求處理時間超過了指定時間,取消執行。
代碼主要涉及 3 個 package,分別是:
開始介紹!
server 負責處理相似 /search?q=golang 的請求,返回 golang 搜索結果,handleSearch 是實際的處理函數,它首先初始化了一個 Context,命名爲 ctx,經過 defer 實現函數退出 cancel。若是請求參數含有 timeout,經過 WithTimeout 建立 context,在超時後,Context 將自動取消。
func handleSearch(w http.ResponseWriter, req *http.Request) {
// ctx is the Context for this handler. Calling cancel closes the
// cxt.Done channel, which is the cancellation signal for requests
// started by this handler
var (
ctx context.Context
cancel context.Context
)
timeout, err := time.ParseDuration(req.FromValue("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 handlSearch returns.
複製代碼
下一步,處理函數會從請求中獲取查詢關鍵詞和客戶端 IP,客戶端 IP 的獲取經過調用 userip 包函數實現。同時,因爲後端服務的請求也須要客戶端 IP,故而將其附在 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.FormRequest(req)
if err != nil {
http.Error(w, e.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)
複製代碼
搜索成功後,handler 渲染結果頁面。
if err := resultsTemplate.Execute(w, struct{
Results google.Results
Timeout, Elapsed time.Duration
}{
Results: results,
Timeout: timeout,
Elaplsed: elaplsed,
}); err != nil {
log.Print(err)
return
}
複製代碼
userip 包中提供了兩個函數,負責從請求中導出用戶 IP 和將用戶 IP 綁定 Context 上。 Context 中包含 key-value 映射,key 與 value 的類型都是 interface{},key 必須支持相等比較,value 要是協程併發安全的。userip 包經過對 Context 中的 value ,即 client IP 執行了類型轉化,隱藏了 map 的細節。爲了不 key 的衝突,userip 定義了一個不可導出的類型 key。
// The key type is unexported to prevent collision with context keys defined in
// other package
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 userIPKye key = 0
複製代碼
函數 FromRequest 負責從 http.Request 導出用戶 IP:
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
}
複製代碼
google.Search 負責 Google Web Search 接口的請求,以及接口返回 JSON 數據的解析。它接收 Context 類型參數 ctx,若是 ctx.Done 關閉,即便請求正在運行也將馬上返回。
查詢的請求參數包括 query 關鍵詞和用戶 IP。
func Search(ctx context.Context, query string) (Results, error) {
// Prepare the Google Search API request
req, err := http.NewRequest("GET", "http://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-users requests
if userIP, ok := userip.FromContext(ctx); ok {
q.Set("userip", userIP.String())
}
req.URL.RawQuery = q.Encode()
複製代碼
Search 函數使用了一個幫助函數,httpDo,負責發起 HTTP 請求,若是 ctx.Done 關閉,即便請求正在執行,也會被關閉。Search 傳遞了一個閉包函數給 httpDo 處理響應結果。
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
})
return results, err
複製代碼
httpDo 函數開啓一個新的 goroutine 負責 HTTP 請求執行和響應結果處理。若是在 goroutine 退出前,即請求還沒執行結束,若是 ctx.Done 關閉,請求執行將被取消。
func httpDo(ctx context.Context, req *http.Request, f func(*http.Request, error) error) error {
// Run the HTTP request in a goroutine and pass the response to f.
c := make(chan error, 1)
req := req.WithContext(ctx)
go func() { c <- f(http.DefaultClient.Do(req)) }()
select {
case <-ctx.Done():
<- c
return ctx.Err
case err := <-c:
return err
}
}
複製代碼
許多服務端框架都提供了相應的包和數據類型進行請求的數據傳遞。咱們能夠基於 Context 接口編寫新的實現代碼,完成框架與處理函數的鏈接。
譯者注:下面介紹的就是開發說的兩個 context 的第三方實現,其中有些內容須要簡單瞭解下它們才能徹底看懂。
複製代碼
例如,Gorilla's 的 context 經過在請求上提供 key value 映射實現關聯數據綁定。在 gorilla.go,提供了 Context 的實現,它的 Value 方法返回的值和一個具體的 HTTP 請求關聯。
其餘一些包提供與 Context 相似的取消支持。例如,Tomb 中有 Kill 方法經過關閉 Dying channel 實現取消信號發出。Tomb 也提供了方法用於等待 goroutine 退出,與 sync.WaitGroup 相似。在 tomb.go 中,提供了一種實現,當父 Context 取消或 Tomb 被 kill時,當前 Context 將會取消。
在 Google,對於接收或發送請求類的函數,咱們要求必需要將 Context 做爲首個參數進行傳遞。如此,即便不一樣團隊的 Go 代碼也能夠工做良好。Context 很是便於 goroutine 的超時與取消控制,以及確保重要數據的安全傳遞,好比安全憑證。
基於 Context 的服務框架須要實現 Context,幫助鏈接框架和使用方,使用方指望從框架接收 Context 參數。而客戶端庫,則與之相反,它從調用方接收 Context 參數。context 經過爲請求數據與取消控制創建通用接口,實現包開發者們能夠很是輕鬆地共享本身的代碼,以及打造出更具擴展性的服務。