《Go語言四十二章經》第三十七章 context包
《Go語言四十二章經》第三十七章 context包
作者:李驍
37.1 context包
在Go中,每個請求的request在單獨的goroutine中進行,處理一個request也可能涉及多個goroutine之間的互動。一個請求衍生出的各個 goroutine 之間需要滿足一定的約束關係,以實現一些諸如有效期,中止routine樹,傳遞請求全域性變數之類的功能。於是Go為我們提供一個解決方案,標準context包。使用context可以使開發者方便的在這些goroutine之間傳遞request相關的資料、取消goroutine的signal或截止時間等。
每個goroutine在執行之前,都要先知道程式當前的執行狀態,通常將這些執行狀態封裝在一個Context變數中,傳遞給要執行的goroutine中。上下文則幾乎已經成為傳遞與請求同生存週期變數的標準方法。在網路程式設計下,當接收到一個網路請求Request,處理Request時,我們可能需要開啟不同的goroutine來獲取資料與邏輯處理,即一個請求Request,會在多個goroutine中處理。而這些goroutine可能需要共享Request的一些資訊;同時當Request被取消或者超時的時候,所有從這個Request建立的所有goroutine也應該被結束。
context包不僅實現了在程式單元之間共享狀態變數的方法,同時能通過簡單的方法,使我們在被呼叫程式單元的外部,通過設定ctx變數值,將過期或撤銷這些訊號傳遞給被呼叫的程式單元。若存在A呼叫B的API,B再呼叫C的API,若A呼叫B取消,那也要取消B呼叫C,通過在A, B, C的API呼叫之間傳遞Context,以及判斷其狀態。
Context結構
// Context包含過期,取消訊號,request值傳遞等,方法在多個goroutine中協程安全 type Context interface { // Done 方法在context被取消或者超時返回一個close的channel Done() <-chan struct{} Err() error // Deadline 返回context超時時間 Deadline() (deadline time.Time, ok bool) // Value 返回context相關key對應的值 Value(key interface{}) interface{} }
-
Deadline會返回一個超時時間,goroutine獲得了超時時間後,例如可以對某些io操作設定超時時間。
-
Done方法返回一個通道(channel),當Context被撤銷或過期時,該通道關閉,即它是一個表示Context是否已關閉的訊號。
-
當Done通道關閉後,Err方法表明Context被撤的原因。
-
Value可以讓goroutine共享一些資料,當然獲得資料是協程安全的。但使用這些資料的時候要注意同步,比如返回了一個map,而這個map的讀寫則要加鎖。
goroutine的建立和呼叫關係總是像層層呼叫進行的,就像人的輩分一樣,而更靠頂部的goroutine應有辦法主動關閉其下屬的goroutine的執行(不然程式可能就失控了)。為了實現這種關係,Context結構也應該像一棵樹,葉子節點須總是由根節點衍生出來的。
要建立Context樹,第一步就是要得到根節點,context.Background函式的返回值就是根節點:
func Background() Context
該函式返回空的Context,該Context一般由接收請求的第一個goroutine建立,是與進入請求對應的Context根節點,它不能被取消、沒有值、也沒有過期時間。它常常作為處理Request的頂層context存在。
有了根節點,又該怎麼建立其它的子節點,孫節點呢?context包為我們提供了多個函式來建立他們:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) func WithValue(parent Context, key interface{}, val interface{}) Context
函式都接收一個Context型別的引數parent,並返回一個Context型別的值,這樣就層層創建出不同的節點。子節點是從複製父節點得到的,並且根據接收引數設定子節點的一些狀態值,接著就可以將子節點傳遞給下層的goroutine了。
再回到之前的問題:該怎麼通過Context傳遞改變後的狀態呢?使用Context的goroutine無法取消某個操作,其實這也是符合常理的,因為這些goroutine是被某個父goroutine建立的,而理應只有父goroutine可以取消操作。在父goroutine中可以通過WithCancel方法獲得一個cancel方法,從而獲得cancel的權利。
第一個WithCancel函式,它是將父節點複製到子節點,並且還返回一個額外的CancelFunc函式型別變數,該函式型別的定義為:
type CancelFunc func()
呼叫CancelFunc物件將撤銷對應的Context物件,這就是主動撤銷Context的方法。在父節點的Context所對應的環境中,通過WithCancel函式不僅可建立子節點的Context,同時也獲得了該節點Context的控制權,一旦執行該函式,則該節點Context就結束了,則子節點需要類似如下程式碼來判斷是否已結束,並退出該goroutine:
select { case <-cxt.Done(): // do some clean... }
WithDeadline函式的作用也差不多,它返回的Context型別值同樣是parent的副本,但其過期時間由deadline和parent的過期時間共同決定。當parent的過期時間早於傳入的deadline時間時,返回的過期時間應與parent相同。父節點過期時,其所有的子孫節點必須同時關閉;反之,返回的父節點的過期時間則為deadline。
WithTimeout函式與WithDeadline類似,只不過它傳入的是從現在開始Context剩餘的生命時長。他們都同樣也都返回了所建立的子Context的控制權,一個CancelFunc型別的函式變數。
當頂層的Request請求函式結束後,我們就可以cancel掉某個context,從而層層goroutine根據判斷cxt.Done()來結束。
WithValue函式,它返回parent的一個副本,呼叫該副本的Value(key)方法將得到val。這樣我們不光將根節點原有的值保留了,還在子孫節點中加入了新的值,注意若存在Key相同,則會被覆蓋。
context包通過構建樹型關係的Context,來達到上一層goroutine能對傳遞給下一層goroutine的控制。對於處理一個Request請求操作,需要採用context來層層控制goroutine,以及傳遞一些變數來共享。
Context物件的生存週期一般僅為一個請求的處理週期。即針對一個請求建立一個Context變數(它為Context樹結構的根);在請求處理結束後,撤銷此ctx變數,釋放資源。
每次建立一個goroutine,要麼將原有的Context傳遞給goroutine,要麼建立一個子Context並傳遞給goroutine。
Context能靈活地儲存不同型別、不同數目的值,並且使多個goroutine安全地讀寫其中的值。
當通過父Context物件建立子Context物件時,可同時獲得子Context的一個撤銷函式,這樣父Context物件的建立環境就獲得了對子Context將要被傳遞到的goroutine的撤銷權。
注意:使用時遵循context規則
-
不要把Context存在一個結構體當中,顯式地傳入函式。Context變數需要作為第一個引數使用,一般命名為ctx。
-
即使方法允許,也不要傳入一個nil的Context,如果你不確定你要用什麼Context的時候傳一個context.TODO。
-
使用context的Value相關方法只應該用於在程式和介面中傳遞的和請求相關的元資料,不要用它來傳遞一些可選的引數。
-
同樣的Context可以用來傳遞到不同的goroutine中,Context在多個goroutine中是安全的。
在子Context被傳遞到的goroutine中,應該對該子Context的Done通道(channel)進行監控,一旦該通道被關閉(即上層執行環境撤銷了本goroutine的執行),應主動終止對當前請求資訊的處理,釋放資源並返回。
37.2 context應用
package main import ( "context" "log" "os" "time" ) var logg *log.Logger func someHandler() { // 新建一個ctx ctx, cancel := context.WithCancel(context.Background()) //傳遞ctx go doStuff(ctx) //10秒後取消doStuff time.Sleep(10 * time.Second) log.Println("cancel") //呼叫cancel:context.WithCancel 返回的CancelFunc cancel() } func doStuff(ctx context.Context) { // for 迴圈來每1秒work一下,判斷ctx是否被取消了,如果是就退出 for { time.Sleep(1 * time.Second) select { case <-ctx.Done(): logg.Printf("done") return default: logg.Printf("work") } } } func main() { logg = log.New(os.Stdout, "", log.Ltime) someHandler() logg.Printf("down") }
程式輸出: 16:28:21 work 16:28:22 work 16:28:23 work 16:28:24 work 16:28:25 work 16:28:26 work 16:28:27 work 16:28:28 work 16:28:29 work 2018/08/22 16:28:30 cancel 16:28:30 down
someHandler() 作為頂層的Request請求函式,處理完主要任務後,主動cancel掉context,而子層goroutinedoStuff(ctx context.Context) 根據判斷cxt.Done()來結束。
本書《Go語言四十二章經》內容在github上同步地址:https://github.com/ffhelicopter/Go42
本書《Go語言四十二章經》內容在簡書同步地址:https://www.jianshu.com/nb/29056963
雖然本書中例子都經過實際執行,但難免出現錯誤和不足之處,煩請您指出;如有建議也歡迎交流。