Go Context

轉自:http://www.javashuo.com/article/p-ezaxcrps-do.html

一、什麼是Context

Context一般被譯做上下文,它是一個比較抽象的概念。在公司技術討論時也常常會提到上下文。通常理解爲程序單元的一個運行狀態、現場、快照,而翻譯中上下又很好地詮釋了其本質,上下上下則是存在上下層的傳遞,會把內容傳遞給。在Go語言中,程序單元也就指的是Goroutine。html

每一個Goroutine在執行以前,都要先知道程序當前的執行狀態,一般將這些執行狀態封裝在一個Context變量中,傳遞給要執行的Goroutine中。上下文則幾乎已經成爲傳遞與請求同生存週期變量的標準方法。在網絡編程下,當接收到一個網絡請求Request,處理Request時,咱們可能須要開啓不一樣的Goroutine來獲取數據與邏輯處理,即一個請求Request,會在多個Goroutine中處理。而這些Goroutine可能須要共享Request的一些信息;同時當Request被取消或者超時的時候,全部從這個Request建立的全部Goroutine也應該被結束。golang

二、context包

Go的設計者早考慮多個Goroutine共享數據,以及多Goroutine管理機制。Context介紹請參考Go Concurrency Patterns: Contextgolang.org/x/net/context包就是這種機制的實現。sql

context包不只實現了在程序單元之間共享狀態變量的方法,同時能經過簡單的方法,使咱們在被調用程序單元的外部,經過設置ctx變量值,將過時或撤銷這些信號傳遞給被調用的程序單元。在網絡編程中,若存在A調用B的API, B再調用C的API,若A調用B取消,那也要取消B調用C,經過在A,B,C的API調用之間傳遞Context,以及判斷其狀態,就能解決此問題,這是爲何gRPC的接口中帶上ctx context.Context參數的緣由之一。編程

Go1.7(當前是RC2版本)已將原來的golang.org/x/net/context包挪入了標準庫中,放在$GOROOT/src/context下面。標準庫中netnet/httpos/exec都用到了context。同時爲了考慮兼容,在原golang.org/x/net/context包下存在兩個文件,go17.go是調用標準庫的context包,而pre_go17.go則是以前的默認實現,其介紹請參考go程序包源碼解讀swift

context包的核心就是Context接口,其定義以下:安全

type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} } 
  • Deadline會返回一個超時時間,Goroutine得到了超時時間後,例如能夠對某些io操做設定超時時間。網絡

  • Done方法返回一個信道(channel),當Context被撤銷或過時時,該信道是關閉的,即它是一個表示Context是否已關閉的信號。ide

  • Done信道關閉後,Err方法代表Context被撤的緣由。函數

  • Value可讓Goroutine共享一些數據,固然得到數據是協程安全的。但使用這些數據的時候要注意同步,好比返回了一個map,而這個map的讀寫則要加鎖。spa

Context接口沒有提供方法來設置其值和過時時間,也沒有提供方法直接將其自身撤銷。也就是說,Context不能改變和撤銷其自身。那麼該怎麼經過Context傳遞改變後的狀態呢?

三、context使用

不管是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的副本,但其過時時間由deadlineparent的過時時間共同決定。當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的撤銷權。

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的執行),應主動終止對當前請求信息的處理,釋放資源並返回。

相關文章
相關標籤/搜索