天天看點

golang中的context詳解一

作者:幹飯人小羽
golang中的context詳解一

1. 背景

我們在開發Golang中的應用時,通常會使用Contexts來控制和管理所依賴的應用中非常重要的資料,例如并發程式設計中的cancellation和data share。

在GoLang中,context作為context的互動的入口,它被認為GoLang中非常重要一個包。假如目前你還沒有遇到與context相關的操作,那麼,相信在不久的将來也肯定會遇到,它的使用非常的廣泛,如果你認真觀察過,你會發現,其它許多的包也會依賴于context。

="https://golang.org/pkg/context">context的官方文檔

2. 正文

2.1 帶value的context

context其中一個比較常用的用法就是共享資料,另外,我們也可以使用request中攜帶的資料。

當你有多個方法/函數之間需要共享資料時,你可以嘗試使用一下context中的方法WithValue(),它的用法非常簡單:context.WithValue。

這個方法的作用: - 基于父context建立一個新的context - 為一個指定的key設定值

你可以簡單了解為,context中包含了一個map,是以你可以根據key來對它進行添加或擷取某個值。

context.WithValue功能比較強大,它可以攜帶任意類型的值,下面我們以一個例子來看一下如何通過context添加、擷取資料。

package main

import (
    "context"
    "fmt"
)

func AddValue(ctx context.Context) context.Context {
    return context.WithValue(ctx, "keyGuan", "this is the value")
}

func GetValue(ctx context.Context, key string) interface{} {
    value := ctx.Value(key)

    return value
}

func main() {
    ctx := context.Background()
    ctx = AddValue(ctx)
    value := GetValue(ctx, "keyGuan")
    fmt.Println(value)
}
           

context的設計哲學: 不變性

所有的context都會傳回一個新的context.Context結構體。這就意味着你必須任何關于context操作的傳回值,并且這些值可能會被一個新的context所覆寫。

關于GoLang中不變性詳情請見我後續的文章

使用這種技術,你可以将context.Context傳遞給其它的并發函數,隻要你正确地管理所傳遞的上下文,這将是在這些并發函數之間共享作用域值的非常好的方法(這意味着每個上下文将在其作用域上保持自己的值)。這正是net/http包在處理HTTP請求時的做法。為了詳細說明這一點,我們來看看下一個例子。

中間件 request scoped data的一個很好的例子是在Web請求處理程式中使用中間件。http.Request類型包含一個context,它可以在整個HTTP管道中攜帶scoped data。 在HTTP管道中添加中間件,然後将中間件的結果添加到http.Request的context中,這是非常常見的代碼。

這是一個非常有用的技術,因為你可以在以後的階段中,依靠那些在pipline中已經确切發生改變的東西。這也使你能夠使用通用代碼來處理http請求,同時也能滿足你想要共享資料的範圍(而不是共享全局變量上的資料)。下面是一個利用請求上下文的中間件的例子:

package main

import (
    "context"
    "fmt"
    "net/http"
    "github.com/google/uuid"
    "github.com/gorilla/mux"
)

func isAlive(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)

    id := r.Context().Value("id")
    fmt.Printf("[%v] Status: 200 - I am live!", id)
    w.Write([]byte("I am alive"))
}

func idMiddleware(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        id := uuid.New()
        r = r.WithContext(context.WithValue(r.Context(), "id", id))
        h.ServeHTTP(w, r)
    })
}
func main() {
    r := mux.NewRouter()
    r.Use(idMiddleware)
    r.HandleFunc("/isalive", isAlive).Methods(http.MethodGet)

    http.ListenAndServe(":8081", r)
}
           

2.2 帶有cancellation的context

說明

context在GoLang中另外一個比較有用的功能是cancellation。當你需要發送一個取消信号時,這個非常有用。同時,當你收到一個取消信号時,你能将其傳遞下去,也是非常關鍵的。

例如,當你在一個函數中建立了上千個goroutine時,main函數将會一直等待所有的goroutine都執行完畢後或取消後才會繼續向下執行;如果你收到了一個取消的信号,比較理想的做法是将之傳遞下去,這樣你就不會浪費計算資源。

針對上面的這個例子,如果能夠在不同的goroutine中共享同一個context的話,将會很容易實作上面的需求。

使用

可以使用context.WithCancel(ctx)建立一個帶有cancellation功能的context。當需要取消的功能時,隻需要調用cancel相關的函數就可以。

示例

假設有這樣一個場景:我們向一個服務發送一個請求 - 如果逾時後還沒有傳回response,我們将發送第二個請求 - 如果能收到任何一個response,所有的請求将會被取消

package main

import (
    "context"
    "fmt"
    "io/ioutil"
    "net/http"
    "time"
    "net/url"
)

func queryWithContext(urls []string) string{
    ch := make(chan string, len(urls))

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    for _, innerURL := range urls {
        go func(u string, c chan string) {
            c <- execWithContext(u, ctx)
        }(innerURL, ch)

        select {
        case r := <-ch:
            cancel()
            return r
        case <-time.After(100 * time.Second):

        }
    }

    return <-ch
}

func execWithContext(url string, ctx context.Context) string {
    start := time.Now()
    pURL, _ := url.Parse(url)
    req := &http.Request{URL: pURL}
    req = req.WithContext(ctx)

    if response, err := http.DefaultClient.Do(req); err == nil {
        defer response.Body.Close()
        body, _ := ioutil.ReadAll(response.Body)

        fmt.Printf("Requst: %d s from url %s\n", time.Since(start).Nanoseconds()/time.Second.Nanoseconds(), url)

        return fmt.Sprintf("%s from %s", body, url)
    } else {
        fmt.Println(err.Error())
        return err.Error()
    }
}
           

每一個請求都是在一個單獨的goroutine中發起,所有的請求中都帶有context,到此我們唯一需要做的就是把請求發送到用戶端。當調用cancel()時,可以優雅的取消請求和底層的連接配接。

對于接受context.Context作為參數的函數來說,這是一個非常常見的模式。它們要麼主動地對context進行操作(比如檢查它是否被取消了),要麼将它傳遞給處理它的底層函數(本例中是通過 http.Request 接收上下文的 Do() 函數)

2.3 帶有timeout的context

這個使用起來比較簡單:

ctx, cancel := context.WithTimeout(context.Background(), 500*time.Second)
           

2.4 gRPC

gRPC的實作也是依賴context的,通過它可以實作資料共享和流控,例如:取消工作流或請求。