Go 中的 context 包在與 API 和慢處理交互時可以派上用場,特別是在生產級的 Web 服務中。在這些場景中,您可能想要通知所有的 goroutine 停止運行並返回。在 Go 語言中 context 包允許您傳遞一個 "context" 到您的程序。 Context 如超時或截止日期(deadline)或通道,來指示停止運行和返回。例如,如果您正在執行一個 web 請求或運行一個系統命令,定義一個超時對生產級系統通常是個好主意。因爲,如果您依賴的API運行緩慢,你不希望在系統上備份(back up)請求,因爲它可能最終會增加負載並降低所有請求的執行效率。導致級聯效應。這是超時或截止日期 context 派上用場的地方。
1、創建 context
context 包允許以下方式創建和獲得 context:
context.Background() Context
這個函數返回一個空 context。這隻能用於高等級(在 main 或頂級請求處理中)。這能用於派生我們稍後談及的其他 context 。
ctx := context.Background()
context.TODO() Context
這個函數也是創建一個空 context。也只能用於高等級或當您不確定使用什麼 context,或函數以後會更新以便接收一個 context 。這意味您(或維護者)計劃將來要添加 context 到函數。
ctx := context.TODO()
有趣的是,查看代碼,它與 background 完全相同。不同的是,靜態分析工具可以使用它來驗證 context 是否正確傳遞,這是一個重要的細節,因爲靜態分析工具可以幫助在早期發現潛在的錯誤,並且可以連接到 CI/CD 管道。
來自 https://golang.org/src/context/context.go:
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
context.WithValue(parent Context, key, val interface{}) (ctx Context, cancel CancelFunc)
此函數接收 context 並返回派生 context,其中值 val 與 key 關聯,並通過 context 樹與 context 一起傳遞。這意味着一旦獲得帶有值的 context,從中派生的任何 context 都會獲得此值。不建議使用 context 值傳遞關鍵參數,而是函數應接收簽名中的那些值,使其顯式化。
ctx := context.WithValue(context.Background(), key, "test")
context.WithCancel(parent Context) (ctx Context, cancel CancelFunc)
這是它開始變得有趣的地方。此函數創建從傳入的父 context 派生的新 context。父 context 可以是後臺 context 或傳遞給函數的 context。
返回派生 context 和取消函數。只有創建它的函數才能調用取消函數來取消此 context。如果您願意,可以傳遞取消函數,但是,強烈建議不要這樣做。這可能導致取消函數的調用者沒有意識到取消 context 的下游影響。可能存在源自此的其他 context,這可能導致程序以意外的方式運行。簡而言之,永遠不要傳遞取消函數。
ctx, cancel := context.WithCancel(context.Background())
context.WithDeadline(parent Context, d time.Time) (ctx Context, cancel CancelFunc)
此函數返回其父項的派生 context,當截止日期超過或取消函數被調用時,該 context 將被取消。例如,您可以創建一個將在以後的某個時間自動取消的 context,並在子函數中傳遞它。當因爲截止日期耗盡而取消該 context 時,獲此 context 的所有函數都會收到通知去停止運行並返回。
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second))
context.WithTimeout(parent Context, timeout time.Duration) (ctx Context, cancel CancelFunc)
此函數類似於 context.WithDeadline。不同之處在於它將持續時間作爲參數輸入而不是時間對象。此函數返回派生 context,如果調用取消函數或超出超時持續時間,則會取消該派生 context。
ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second)
2、函數接收和使用 Context
現在我們知道了如何創建 context(Background 和 TODO)以及如何派生 context(WithValue,WithCancel,Deadline 和 Timeout),讓我們討論如何使用它們。
在下面的示例中,您可以看到接受 context 的函數啓動一個 goroutine 並等待 該 goroutine 返回或該 context 取消。select 語句幫助我們選擇先發生的任何情況並返回。
<-ctx.Done()
一旦 Done 通道被關閉,這個 <-ctx.Done():
被選擇。一旦發生這種情況,此函數應該放棄運行並準備返回。這意味着您應該關閉所有打開的管道,釋放資源並從函數返回。有些情況下,釋放資源可以阻止返回,比如做一些掛起的清理等等。在處理 context 返回時,您應該注意任何這樣的可能性。
3、實例
實例1
package main
import (
"fmt"
"golang.org/x/net/context"
"time"
)
func main() {
//context傳值操作:Context能靈活地存儲不同類型、不同數目的值,並且使多個Goroutine安全地讀寫其中的值。
myContext := context.WithValue(context.Background(), "key", "001") //對context進行傳值
fmt.Println("傳遞的值是:",myContext.Value("key")) //接受context傳遞的值
//context開關作用
ctx, cancel := context.WithCancel(context.Background())
i:=0
go func() {
for {
select {
case <-ctx.Done():
return
default:
time.Sleep(time.Second)
fmt.Println("=============",i,"=================")
i++
// todo 需要執行的操作
}
}
}()
//10s之後調用context,結束go程
for {
time.Sleep(time.Second*10)
cancel() //調用context取消(執行<-ctx.Done()被觸發)
break
}
fmt.Println("====執行結束===")
}
上面代碼,就是context的簡單使用,context.WithCancel(context.Background())會返回一個context,以及cancel函數,context作用就是插入goroutine中的,如果ctx.Done()有信號,那麼就執行return,以達到掐斷goroutine的目的。那什麼時候ctx.Done觸發呢?那就要看cancel函數什麼時候被調用了。其實cancel函數被調用,ctx.Done就被觸發。上面例子,就是簡單的讓主goroutine“睡10秒”,就調用cancel(),那麼就是過了10秒後,之前創建的goroutine裏面的ctx.Done就有信號了,goroutine退出。可以看出cancel就是一個開關,可以關掉一個或多個goroutine。context是線程安全的。例如,exec.CommandContext 不會關閉讀取管道,直到命令執行了進程創建的所有分支(Github 問題:https://github.com/golang/go/issues/23019 ),這意味着如果等待 cmd.Wait() 直到外部命令的所有分支都已完成,則 context 取消不會使該函數立即返回。如果您使用超時或截止日期,您可能會發現這不能按預期運行。如果遇到任何此類問題,可以使用 time.After 實現超時。
3.2搜索測試程序
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.
tr := &http.Transport{}
client := &http.Client{Transport: tr}
c := make(chan error, 1)
go func() { c <- f(client.Do(req)) }()
select {
case <-ctx.Done():
tr.CancelRequest(req)
<-c // Wait for f to return.
return ctx.Err()
case err := <-c:
return err
}
}
httpDo關鍵的地方在於
select {
case <-ctx.Done():
tr.CancelRequest(req)
<-c // Wait for f to return.
return ctx.Err()
case err := <-c:
return err
}
要麼ctx被取消,要麼request請求出錯。
4、小結
context包通過構建樹型關係的Context,來達到上一層Goroutine能對傳遞給下一層Goroutine的控制。對於處理一個Request請求操作,需要採用context來層層控制Goroutine,以及傳遞一些變量來共享。
Context對象的生存週期一般僅爲一個請求的處理週期。即針對一個請求創建一個Context變量(它爲Context樹結構的根);在請求處理結束後,撤銷此ctx變量,釋放資源。
每次創建一個Goroutine,要麼將原有的Context傳遞給Goroutine,要麼創建一個子Context並傳遞給Goroutine。
Context能靈活地存儲不同類型、不同數目的值,並且使多個Goroutine安全地讀寫其中的值。
當通過父Context對象創建子Context對象時,可同時獲得子Context的一個撤銷函數,這樣父Context對象的創建環境就獲得了對子Context將要被傳遞到的Goroutine的撤銷權。
5 、使用原則
Programs that use Contexts should follow these rules to keep interfaces consistent across packages and enable static analysis tools to check context propagation:使用Context的程序包需要遵循如下的原則來滿足接口的一致性以及便於靜態分析。
Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx;不要把Context存在一個結構體當中,顯式地傳入函數。Context變量需要作爲第一個參數使用,一般命名爲ctx;
Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use;即使方法允許,也不要傳入一個nil的Context,如果你不確定你要用什麼Context的時候傳一個context.TODO;
Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions;使用context的Value相關方法只應該用於在程序和接口中傳遞的和請求相關的元數據,不要用它來傳遞一些可選的參數;
The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines;同樣的Context可以用來傳遞到不同的goroutine中,Context在多個goroutine中是安全的;
在子Context被傳遞到的goroutine中,應該對該子Context的Done信道(channel)進行監控,一旦該信道被關閉(即上層運行環境撤銷了本goroutine的執行),應主動終止對當前請求信息的處理,釋放資源並返回。