Context 通常被譯作 上下文 ,一般理解為程序單元的一個運行狀態、現場、快照,而翻譯中 上下 又很好地詮釋了其本質,上下上下則是存在上下層的傳遞, 上 會把內容傳遞給 下 。
在Go語言中,程序單元也就指的是Goroutine。每個Goroutine在執行之前,都要先知道程序當前的執行狀態,通常將這些執行狀態封裝在一個Context 變量中,傳遞給要執行的Goroutine中。上下文則幾乎已經成為傳遞與請求同生存周期變量的標准方法。
context 包不僅實現了在程序單元之間共享狀態變量的方法,同時能通過簡單的方法,使我們在被調用程序單元的外部,通過設置ctx變量值,將過期或撤銷這些信號傳遞給被調用的程序單元。在網絡編程中,若存在A調用B的API, B再調用C的API,若A調用B取消,那也要取消B調用C,通過在A,B,C的API調用之間傳遞 Context ,以及判斷其狀態,就能解決此問題,這是為什么gRPC的接口中帶上 ctx context.Context 參數的原因之一。
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是否已關閉的信號。 -
當
Done信道關閉后,Err方法表明Context被撤的原因。 -
Value可以讓Goroutine共享一些數據,當然獲得數據是協程安全的。但使用這些數據的時候要注意同步,比如返回了一個map,而這個map的讀寫則要加鎖。
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 的副本,但其過期時間由 deadline 和 parent 的過期時間共同決定。當 parent 的過期時間早於傳入的 deadline 時間時,返回的過期時間應與 parent 相同。父節點過期時,其所有的子孫節點必須同時關閉;反之,返回的父節點的過期時間則為 deadline 。
WithTimeout 函數與 WithDeadline 類似,只不過它傳入的是從現在開始Context剩余的生命時長。他們都同樣也都返回了所創建的子Context的控制權,一個 CancelFunc 類型的函數變量。
當頂層的Request請求函數結束后,我們就可以cancel掉某個context,從而層層Goroutine根據判斷 cxt.Done() 來結束。
WithValue 函數,它返回 parent 的一個副本,調用該副本的Value(key)方法將得到val。這樣我們不光將根節點原有的值保留了,還在子孫節點中加入了新的值,注意若存在Key相同,則會被覆蓋。
范例:
package main import ( "fmt" "time" "golang.org/x/net/context" ) func main() { // ctx, cancelFunc := context.WithDeadline(context.Background(), time.Now().Add(time.Second*5)) ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5) ctx = context.WithValue(ctx, "Test", "123456") // defer cancelFunc() if t, ok := ctx.Deadline(); ok { fmt.Println(time.Now()) fmt.Println(t.String()) } go func(ctx context.Context) { fmt.Println(ctx.Value("Test")) for { select { case <-ctx.Done(): fmt.Println(ctx.Err()) return // default: // continue } } }(ctx) // if ctx.Err() == nil { // fmt.Println("Sleep 10 seconds...") // time.Sleep(time.Second * 10) // } // if ctx.Err() != nil { // fmt.Println("Alredy exit...") // } time.Sleep(time.Second * 3) cancelFunc() // for { // if ctx.Err() != nil { // fmt.Println("gracefully exit...") // break // } // } }
參考:http://lanlingzi.cn/post/technical/2016/0802_go_context/?utm_source=tuicool&utm_medium=referral
http://blog.csdn.net/u014029783/article/details/53782864
http://blog.csdn.net/zdyueguanyun/article/details/64904703
