Go 部落格
Go 併發模式:Context
引言
在 Go 伺服器中,每個傳入請求都在自己的 goroutine 中處理。請求處理程式通常會啟動額外的 goroutine 來訪問後端,例如資料庫和 RPC 服務。處理請求的 goroutine 集合通常需要訪問特定於請求的值,例如終端使用者的身份、授權令牌和請求的截止日期。當請求被取消或超時時,所有處理該請求的 goroutine 都應該快速退出,以便系統可以回收它們正在使用的任何資源。
在 Google,我們開發了一個 context
包,可以輕鬆地在 API 邊界上傳遞請求範圍的值、取消訊號和截止日期,傳遞給所有參與處理請求的 goroutine。該包作為 context 公開可用。本文介紹瞭如何使用該包並提供了一個完整的示例。
Context
context
包的核心是 Context
型別
// A Context carries a deadline, cancellation 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{} }
(此描述為精簡版;godoc 是權威的。)
Done
方法返回一個通道,該通道充當為 Context
執行的函式的取消訊號:當通道關閉時,函式應該放棄其工作並返回。Err
方法返回一個錯誤,指示 Context
被取消的原因。管道和取消文章更詳細地討論了 Done
通道慣用語。
Context
沒有 Cancel
方法,原因與 Done
通道是隻接收通道的原因相同:接收取消訊號的函式通常不是傳送訊號的函式。特別是,當父操作為子操作啟動 goroutine 時,這些子操作不應該能夠取消父操作。相反,WithCancel
函式(如下所述)提供了一種取消新的 Context
值的方法。
Context
可以安全地供多個 goroutine 同時使用。程式碼可以將單個 Context
傳遞給任意數量的 goroutine,並取消該 Context
以向所有 goroutine 發出訊號。
Deadline
方法允許函式判斷它們是否應該開始工作;如果剩餘時間太少,可能就不值得。程式碼還可以使用截止日期來設定 I/O 操作的超時。
Value
允許 Context
攜帶請求範圍的資料。該資料必須安全地供多個 goroutine 同時使用。
派生上下文
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
返回派生 Context
值,這些值可以比父 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. 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
瞭解如何使用 context
包的最佳方法是透過一個例項。
示例:Google 網頁搜尋
我們的示例是一個 HTTP 伺服器,它透過將查詢“golang”轉發到 Google 網頁搜尋 API 並呈現結果來處理像 /search?q=golang&timeout=1s
這樣的 URL。timeout
引數告訴伺服器在該持續時間過去後取消請求。
程式碼分為三個包
- server 提供
main
函式和/search
的處理程式。 - userip 提供從請求中提取使用者 IP 地址並將其與
Context
關聯的函式。 - google 提供
Search
函式,用於向 Google 傳送查詢。
伺服器程式
server 程式透過提供 golang
的前幾個 Google 搜尋結果來處理諸如 /search?q=golang
之類的請求。它註冊 handleSearch
來處理 /search
端點。處理程式建立一個名為 ctx
的初始 Context
,並安排在處理程式返回時取消它。如果請求包含 timeout
URL 引數,則在超時過去後,Context
會自動取消
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
和 query
呼叫 google.Search
// 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 地址並將其與 Context
關聯的函式。Context
提供了一個鍵值對映,其中鍵和值都是 interface{}
型別。鍵型別必須支援相等性,並且值必須安全地供多個 goroutine 同時使用。像 userip
這樣的包隱藏了此對映的詳細資訊,並提供了對特定 Context
值的強型別訪問。
為避免鍵衝突,userip
定義了一個未匯出型別 key
,並使用此型別的值作為上下文鍵
// 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
FromRequest
從 http.Request
中提取 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) { // 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 包
google.Search 函式向 Google 網頁搜尋 API 發出 HTTP 請求並解析 JSON 編碼的結果。它接受一個 Context
引數 ctx
,並且如果在請求進行中 ctx.Done
關閉,則立即返回。
Google 網頁搜尋 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
關閉時取消請求。Search
將一個閉包傳遞給 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
httpDo
函式在一個新的 goroutine 中執行 HTTP 請求並處理其響應。如果在 goroutine 退出之前 ctx.Done
關閉,它會取消請求
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. 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 } }
調整程式碼以適應 Context
許多伺服器框架提供用於攜帶請求範圍值的包和型別。我們可以定義 Context
介面的新實現,以橋接使用現有框架的程式碼和期望 Context
引數的程式碼。
例如,Gorilla 的 github.com/gorilla/context 包允許處理程式透過提供 HTTP 請求到鍵值對的對映來將資料與傳入請求關聯起來。在 gorilla.go 中,我們提供了一個 Context
實現,其 Value
方法返回 Gorilla 包中與特定 HTTP 請求關聯的值。
其他包提供了與 Context
類似的取消支援。例如,Tomb 提供了一個 Kill
方法,透過關閉 Dying
通道來發出取消訊號。Tomb
還提供了等待這些 goroutine 退出的方法,類似於 sync.WaitGroup
。在 tomb.go 中,我們提供了一個 Context
實現,當其父 Context
被取消或提供的 Tomb
被殺死時,它會被取消。
結論
在 Google,我們要求 Go 程式設計師將 Context
引數作為呼叫路徑中傳入和傳出請求之間每個函式的第一個引數傳遞。這使得許多不同團隊開發的 Go 程式碼能夠很好地協同工作。它提供了對超時和取消的簡單控制,並確保安全憑據等關鍵值能夠正確地透過 Go 程式傳輸。
希望基於 Context
構建的伺服器框架應該提供 Context
的實現,以橋接它們的包和那些期望 Context
引數的包。它們的客戶端庫將接受來自呼叫程式碼的 Context
。透過為請求範圍資料和取消建立通用介面,Context
使包開發人員更容易共享程式碼以建立可伸縮服務。
延伸閱讀
下一篇文章:OSCON 上的 Go
上一篇文章:Go 將參加 OSCON 2014
部落格索引