GoLang中的Context
1. 背景
我們在開發Golang中的應用時,通常會使用Contexts來控制和管理所依賴的應用中非常重要的資料,例如併發程式設計中的cancellation
和data share
。
在GoLang中,context
作為context的互動的入口,它被認為GoLang中非常重要一個包。假如當前你還沒有遇到與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
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
的,通過它可以實現資料共享和流控,例如:取消工作流或請求。
更多關於gRPC相關的內容請檢視官網。