https://github.com/gorilla/context

作者:Sameer Ajmani | 地址:blog.golang.org/context

譯者前言

第二篇官方博客的翻譯,主要是關於 Go 併發控制的 context 包。

總體來說,我認爲上一篇纔是 Go 併發的基礎與核心。context 是在前章基礎之上,爲 goroutine 控制而開發的一套便於使用的庫。畢竟,在不同的 goroutine 之間只傳遞 done channel,包含信息量確實是太少。

文章簡單介紹了 context 提供的方法,以及簡單介紹它們如何使用。接着,通過一個搜索的例子,介紹了在真實場景下的使用。

文章的尾部部分說明了,除了官方實現的 context,也有一些第三方的實現,比如 github.com/contextTomb,但這些在官方 context 出現之後就已經停止更新了。其實原因很簡單,畢竟一般都是官方更強大。之前,go 模塊管理也是百花齊放,但最近官方推出自己的解決方案,或許不久,其他方式都將會淘汰。

其實,我覺得這篇文章並不好讀,感覺不夠循序漸進。突然的一個例子或許會讓人有點懵逼。

翻譯正文如下:


Go 的服務中,每個請求都會有獨立的 goroutine 處理,每個 goroutine 通常會啓動新的 goroutine 執行一些額外的工作,比如進行數據庫或 RPC 服務的訪問。同請求內的 goroutine 需能共享請求數據訪問,比如,用戶認證,授權 token,以及請求截止時間。如果請求取消或發生超時,請求範圍內的所有 goroutine 都應立刻退出,進行資源回收.

在 Google,我們開發了一個 context 的包,通過它,我們可以非常方便地在請求內的 goroutine 之間傳遞請求數據、取消信號和超時信息。詳情查看 context

本文將會具體介紹 context 包的使用,並提供一個完整的使用案例。

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 取消,派生的 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 如何使用呢?最好的方式,通過一個案例演示。

案例:Google Web 搜索

演示一個案例,實現一個 HTTP 服務,處理類似 /search?q=golang&timeout=1s 的請求。timeout 表示如果請求處理時間超過了指定時間,取消執行。

代碼主要涉及 3 個 package,分別是:

  • server,主函數入口和 /search 處理函數;
  • userip,實現從 request 的 context 導出 user ip 的公共函數;
  • google,實現了 Search 函數,負責向 Google 發送搜索請求;

開始介紹!

Package server

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
    }

Package userip

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
}

Package google

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 接口編寫新的實現代碼,完成框架與處理函數的連接。

譯者注:下面介紹的就是開發說的兩個 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 通過爲請求數據與取消控制建立通用接口,實現包開發者們可以非常輕鬆地共享自己的代碼,以及打造出更具擴展性的服務。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章