golang中Context的使用場景

2021-03-02 軒脈刃的刀光劍影
golang中Context的使用場景

context在Go1.7之後就進入標準庫中了。它主要的用處如果用一句話來說,是在於控制goroutine的生命周期。當一個計算任務被goroutine承接了之後,由於某種原因(超時,或者強制退出)我們希望中止這個goroutine的計算任務,那麼就用得到這個Context了。

關於Context的四種結構,CancelContext,TimeoutContext,DeadLineContext,ValueContext的使用在這一篇快速掌握 Golang context 包已經說的很明白了。

本文主要來盤一盤golang中context的一些使用場景:

場景一:RPC調用

在主goroutine上有4個RPC,RPC2/3/4是並行請求的,我們這裡希望在RPC2請求失敗之後,直接返回錯誤,並且讓RPC3/4停止繼續計算。這個時候,就使用的到Context。

這個的具體實現如下面的代碼。

package main

import (
    "context"
    "sync"
    "github.com/pkg/errors"
)

func Rpc(ctx context.Context, url string) error {
    result := make(chan int)
    err := make(chan error)

    go func() {
        
        isSuccess := true
        if isSuccess {
            result <- 1
        } else {
            err <- errors.New("some error happen")
        }
    }()

    select {
        case <- ctx.Done():
            
            return ctx.Err()
        case e := <- err:
            
            return e
        case <- result:
            
            return nil
    }
}


func main() {
    ctx, cancel := context.WithCancel(context.Background())

    
    err := Rpc(ctx, "http://rpc_1_url")
    if err != nil {
        return
    }

    wg := sync.WaitGroup{}

    
    wg.Add(1)
    go func(){
        defer wg.Done()
        err := Rpc(ctx, "http://rpc_2_url")
        if err != nil {
            cancel()
        }
    }()

    
    wg.Add(1)
    go func(){
        defer wg.Done()
        err := Rpc(ctx, "http://rpc_3_url")
        if err != nil {
            cancel()
        }
    }()

    
    wg.Add(1)
    go func(){
        defer wg.Done()
        err := Rpc(ctx, "http://rpc_4_url")
        if err != nil {
            cancel()
        }
    }()

    wg.Wait()
}

當然我這裡使用了waitGroup來保證main函數在所有RPC調用完成之後才退出。

在Rpc函數中,第一個參數是一個CancelContext, 這個Context形象的說,就是一個傳話筒,在創建CancelContext的時候,返回了一個聽聲器(ctx)和話筒(cancel函數)。所有的goroutine都拿著這個聽聲器(ctx),當主goroutine想要告訴所有goroutine要結束的時候,通過cancel函數把結束的信息告訴給所有的goroutine。當然所有的goroutine都需要內置處理這個聽聲器結束信號的邏輯(ctx->Done())。我們可以看Rpc函數內部,通過一個select來判斷ctx的done和當前的rpc調用哪個先結束。

這個waitGroup和其中一個RPC調用就通知所有RPC的邏輯,其實有一個包已經幫我們做好了。errorGroup。具體這個errorGroup包的使用可以看這個包的test例子。

有人可能會擔心我們這裡的cancel()會被多次調用,context包的cancel調用是冪等的。可以放心多次調用。

我們這裡不妨品一下,這裡的Rpc函數,實際上我們的這個例子裡面是一個「阻塞式」的請求,這個請求如果是使用http.Get或者http.Post來實現,實際上Rpc函數的Goroutine結束了,內部的那個實際的http.Get卻沒有結束。所以,需要理解下,這裡的函數最好是「非阻塞」的,比如是http.Do,然後可以通過某種方式進行中斷。比如像這篇文章Cancel http.Request using Context中的這個例子:

func httpRequest(
  ctx context.Context,
  client *http.Client,
  req *http.Request,
  respChan chan []byte,
  errChan chan error
) {
  req = req.WithContext(ctx)
  tr := &http.Transport{}
  client.Transport = tr
  go func() {
    resp, err := client.Do(req)
    if err != nil {
      errChan <- err
    }
    if resp != nil {
      defer resp.Body.Close()
      respData, err := ioutil.ReadAll(resp.Body)
      if err != nil {
        errChan <- err
      }
      respChan <- respData
    } else {
      errChan <- errors.New("HTTP request failed")
    }
  }()
  for {
    select {
    case <-ctx.Done():
      tr.CancelRequest(req)
      errChan <- errors.New("HTTP request cancelled")
      return
    case <-errChan:
      tr.CancelRequest(req)
      return
    }
  }
}

它使用了http.Client.Do,然後接收到ctx.Done的時候,通過調用transport.CancelRequest來進行結束。
我們還可以參考net/dail/DialContext
換而言之,如果你希望你實現的包是「可中止/可控制」的,那麼你在你包實現的函數裡面,最好是能接收一個Context函數,並且處理了Context.Done。

場景二:PipeLine

pipeline模式就是流水線模型,流水線上的幾個工人,有n個產品,一個一個產品進行組裝。其實pipeline模型的實現和Context並無關係,沒有context我們也能用chan實現pipeline模型。但是對於整條流水線的控制,則是需要使用上Context的。這篇文章Pipeline Patterns in Go的例子是非常好的說明。這裡就大致對這個代碼進行下說明。

runSimplePipeline的流水線工人有三個,lineListSource負責將參數一個個分割進行傳輸,lineParser負責將字符串處理成int64,sink根據具體的值判斷這個數據是否可用。他們所有的返回值基本上都有兩個chan,一個用於傳遞數據,一個用於傳遞錯誤。(<-chan string, <-chan error)輸入基本上也都有兩個值,一個是Context,用於傳聲控制的,一個是(in <-chan)輸入產品的。

我們可以看到,這三個工人的具體函數裡面,都使用switch處理了case <-ctx.Done()。這個就是生產線上的命令控制。

func lineParser(ctx context.Context, base int, in <-chan string) (
    <-chan int64, <-chan error, error) {
    ...
    go func() {
        defer close(out)
        defer close(errc)

        for line := range in {

            n, err := strconv.ParseInt(line, base, 64)
            if err != nil {
                errc <- err
                return
            }

            select {
            case out <- n:
            case <-ctx.Done():
                return
            }
        }
    }()
    return out, errc, nil
}

場景三:超時請求

我們發送RPC請求的時候,往往希望對這個請求進行一個超時的限制。當一個RPC請求超過10s的請求,自動斷開。當然我們使用CancelContext,也能實現這個功能(開啟一個新的goroutine,這個goroutine拿著cancel函數,當時間到了,就調用cancel函數)。

鑑於這個需求是非常常見的,context包也實現了這個需求:timerCtx。具體實例化的方法是 WithDeadline 和 WithTimeout。

具體的timerCtx裡面的邏輯也就是通過time.AfterFunc來調用ctx.cancel的。

官方的例子:

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
    defer cancel()

    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err()) 
    }
}

在http的客戶端裡面加上timeout也是一個常見的辦法

uri := "https://httpbin.org/delay/3"
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
    log.Fatalf("http.NewRequest() failed with '%s'\n", err)
}

ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*100)
req = req.WithContext(ctx)

resp, err := http.DefaultClient.Do(req)
if err != nil {
    log.Fatalf("http.DefaultClient.Do() failed with:\n'%s'\n", err)
}
defer resp.Body.Close()

在http服務端設置一個timeout如何做呢?

package main

import (
    "net/http"
    "time"
)

func test(w http.ResponseWriter, r *http.Request) {
    time.Sleep(20 * time.Second)
    w.Write([]byte("test"))
}


func main() {
    http.HandleFunc("/", test)
    timeoutHandler := http.TimeoutHandler(http.DefaultServeMux, 5 * time.Second, "timeout")
    http.ListenAndServe(":8080", timeoutHandler)
}

我們看看TimeoutHandler的內部,本質上也是通過context.WithTimeout來做處理。

func (h *timeoutHandler) ServeHTTP(w ResponseWriter, r *Request) {
  ...
        ctx, cancelCtx = context.WithTimeout(r.Context(), h.dt)
        defer cancelCtx()
    ...
    go func() {
    ...
        h.handler.ServeHTTP(tw, r)
    }()
    select {
    ...
    case <-ctx.Done():
        ...
    }
}

場景四:HTTP伺服器的request互相傳遞數據

context還提供了valueCtx的數據結構。

這個valueCtx最經常使用的場景就是在一個http伺服器中,在request中傳遞一個特定值,比如有一個中間件,做cookie驗證,然後把驗證後的用戶名存放在request中。

我們可以看到,官方的request裡面是包含了Context的,並且提供了WithContext的方法進行context的替換。

package main

import (
    "net/http"
    "context"
)

type FooKey string

var UserName = FooKey("user-name")
var UserId = FooKey("user-id")

func foo(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ctx := context.WithValue(r.Context(), UserId, "1")
        ctx2 := context.WithValue(ctx, UserName, "yejianfeng")
        next(w, r.WithContext(ctx2))
    }
}

func GetUserName(context context.Context) string {
    if ret, ok := context.Value(UserName).(string); ok {
        return ret
    }
    return ""
}

func GetUserId(context context.Context) string {
    if ret, ok := context.Value(UserId).(string); ok {
        return ret
    }
    return ""
}

func test(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("welcome: "))
    w.Write([]byte(GetUserId(r.Context())))
    w.Write([]byte(" "))
    w.Write([]byte(GetUserName(r.Context())))
}

func main() {
    http.Handle("/", foo(test))
    http.ListenAndServe(":8080", nil)
}

在使用ValueCtx的時候需要注意一點,這裡的key不應該設置成為普通的String或者Int類型,為了防止不同的中間件對這個key的覆蓋。最好的情況是每個中間件使用一個自定義的key類型,比如這裡的FooKey,而且獲取Value的邏輯儘量也抽取出來作為一個函數,放在這個middleware的同包中。這樣,就會有效避免不同包設置相同的key的衝突問題了。

參考

快速掌握 Golang context 包
視頻筆記:如何正確使用 Context - Jack Lindamood
Go Concurrency Patterns: Context
Cancel http.Request using Context
Pipeline Patterns in Go

相關焦點

  • golang之context使用
    背景golang中並發編程的三種實現方式:chan管道、waitGroup和Context。本篇將重點介紹context的使用,告訴大家基本的使用方式,做到會用。Context概念介紹context譯為上下文,golang在1.6.2的時候還沒有自己的context,在1.7的版本中就把golang.org/x/net/context包被加入到了官方的庫中。golang 的 Context包,是專門用來處理多個goroutine之間與請求域的數據、取消信號、截止時間等相關操作。
  • 理解 Golang Context 機制
    【導讀】golang編程中經常用到Context,理解context設計和掌握其機制,對實現我們需要的功能有很大幫助。本文詳細介紹了go Context。1.golang.org/x/net/context包就是這種機制的實現。context包實現的主要功能為:其一,在程序單元之間共享狀態變量。
  • 【Golang】Context基礎篇
    一個接口golang的context包定義了Context類型,根據官方文檔的說法,該類型被設計用來在API邊界之間以及過程之間傳遞截止時間
  • Golang context你必須要知道的
    go1.6 及之前版本請使用 golang.org/x/net/context。go1.7 及之後已移到標準庫 context。使用 context 的 Value 相關方法只應該用於在程序和接口中傳遞的和請求相關的元數據,不要用它來傳遞一些可選的參數。相同的 Context 可以傳遞給在不同的 goroutine;Context 是並發安全的。
  • 走進Golang之Context的使用
    那麼面對這個場景我們需要哪些額外的事情呢?Golang 為我們準備好了一切,就是 context.Context 這個包,這個包的原始碼非常簡單,源碼部分本文會略過,下期單獨一篇文章來講,本篇我們重點談正確的使用。Context 的結構非常簡單,它是一個接口。
  • Golang context使用經典案例
    在 go 伺服器中,對於每個請求的 request 都是在單獨的
  • gRPC 實操指南(golang)
    1.2 RPC業務場景RPC的應用場景很廣泛:•所有的分布式機都需要進行登陸的驗證,對於所有的主機都實現相同的登陸驗證邏輯維護極差,同時也失去部分分布式意義,所以從解耦的角度考慮,我們需要定義一個統一的登陸驗證業務來做。
  • golang下文件鎖的使用
    前言題目是golang下文件鎖的使用,但本文的目的其實是通過golang下的文件鎖的使用方法,來一窺文件鎖背後的機制。
  • Golang後臺單元測試實踐
    並且能在單測階段發現問題,修復的成本也是最小的,不必等到聯調測試中發現。另一方面在寫單測的過程中也能夠反思業務代碼的正確性、合理性,能推動我們在實現的過程中更好地反思代碼的設計並及時調整。golang原生testing框架特點文件形式:文件以_test.go 結尾函數形式:func TestXxx(*testing.T)斷言:使用 t.Errorf 或相關方法來發出失敗信號運行:使用go test –v執行單元測試示例// 原函數 (in add.go)func
  • 使用Golang快速構建WEB應用
    — 阿飛希望大家都變卡卡西。 — 啊賤大家copy愉快,文檔只做參考。如果發現問題或者有好的建議請回復我我回及時更正。 1.Abstract在學習web開發的過程中會遇到很多困難,因此寫了一篇類似綜述類的文章。
  • 更便捷的goroutine控制利器- Context
    let『s GO在本文中,我首先會介紹context是什麼,它有什麼作用,以及如何使用,其中還會參雜一點個人的理解,以及部分源碼的了解。What are you waiting for?審核工具檢查所有控制流路徑上是否都使用了CancelFuncs。使用上下文的程序應遵循以下規則,以使各個包之間的接口保持一致,並使靜態分析工具可以檢查上下文傳播:不要將上下文存儲在結構類型中;而是將上下文明確傳遞給需要它的每個函數。
  • Golang入門教程——map篇
    今天是golang專題的第7篇文章,我們來聊聊golang當中map的用法。map這個數據結構我們經常使用,存儲的是key-value的鍵值對。在C++/java當中叫做map,在Python中叫做dict。
  • 營銷中常說的「context」到底幾個意思?
    註:該圖非真實截屏,純屬舉個慄子越來越多廣告主意識到了context的重要性。這兩年,在營銷中context這個詞越來越多被提及。內容環境這一層context對廣告效果的影響,不僅是投放階段的挑戰,在投放前的廣告測試階段亦然。傳統的廣告測試中,受訪者被要求認真地觀看廣告,完了填答問卷。但在實際的媒體環境中,消費者不一定能「認真地觀看廣告」。廣告主希望在更真實的媒介環境中開展廣告測試,以獲得更準確的結果。有一次客戶將在新聞客戶端上投放信息流廣告,需要在6個軟文標題的備選方案中挑一個。
  • Golang入門教程——面向對象篇
    golang作為一門剛剛誕生十年的新興語言自然是支持面向對象的,但是golang當中面向對象的概念和特性與我們之前熟悉的大部分語言都不盡相同。比如Java、Python等,相比之下, golang這個部分的設計非常得簡潔和優雅(仁者見仁),所以即使你之前沒有系統地了解過面向對象,也沒有關係,也一定能夠看懂。
  • Golang語言標準庫 sync 包的 Once 怎麼使用?
    04踩坑我們已經介紹過 Once 是一個只執行一次操作的對象,假如我們在 Do 方法中再次調用 Do 方法會怎麼樣呢?代碼如下:所以,記住不要在Do 方法的給定參數中,調用 Do 方法,否則會產生死鎖。05總結本文開篇介紹了 Once 的官方定義和使用場景,然後結合示例代碼,介紹了 Once 的基本使用,並通過閱讀源碼,介紹了 Once 的實現原理,最後列舉了一個容易踩的「坑」。
  • Golang入門教程——基本操作篇
    在golang的設計中設想當中,只需要一種循環,就可以實現所有的功能。從某種程度上來說,也的確如此,golang中的循環有點像是C++和Python循環的結合體,集合兩種所長。首先,我們先來看下for循環的語法,在for循環當中,我們使用分號分開循環條件。
  • Golang 中 nil==nil 是對是錯?
    via:https://medium.com/@shivi28/go-when-nil-nil-returns-true-a8a014abeffbauthor:Shivani Singhal這篇文章,我們將了解如何在 Go 中使用 == 操作符比較對象值。
  • 語義分割 | context relation
    這顯然不符合人類對事物的認知方式,人腦中的分割類似於:把一個場景中相似的像素聚成一團,然後宏觀的判斷這一團像素是什麼,在判斷類別時,還會利用類別之間的依賴關係聯合推理。肯定不會一個點一個點的看這是什麼。
  • 「Golang」for range 使用方法及避坑指南
    在Go中,提供了兩種循環控制結構for和goto,但是後者不推薦使用(原因請查看艾茲格·迪傑斯特拉[2](Edsger Wybe Dijkstra)在1968年的一篇名稱為《GOTO語句有害論》的論文),但是就作者而言goto在某些業務情況下,是很好用的,所以也不需要完全就反對他。
  • Golang:重新認識你的Go應用
    執行如上命令,找到應用程式的入口地址,到golang的runtime包中分析源碼,梳理其啟動過程如下:G--GoroutineGo語言調度器中待執行的任務,在運行時調度器中的地位,與線程在作業系統中差不多,但是它佔用了更小的內存空間,也降低了上下文切換的開銷。Goroutine只存在於Go語言的運行時,是Go語言在用戶態提供的線程,作為一種顆粒度更細的資源調度單元,如果使用得當,能夠在高並發的場景下更高效地利用機器的CPU。