這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
context是隱式的約束,沒有檢測
如果我們寫一個函數,比如:
func f(a int, b []byte) {}
我們知道它需要哪些參數,編譯器是會幫我做檢查的,當我調用
f(3, "sdfsdf")
它就會報錯。
可是如果是context,就變成了一種隱式的約束,編譯器不會幫我們做檢查,比如:
func f(ctx context.Context) { a := ctx.Value("a").(int) b := ctx.Value("b").([]byte)}
Value函數並沒有任何保證,編譯器不會檢查傳進來的參數是否是合理。然而f在什麼樣的上下文裡面被調用是不確定的,因此檢測被移到了運行時來做。
現在的函數f有一個隱式的約束,它需要從context裡面傳a和b兩個參數,這些資訊,在函數f的簽名裡面都沒法體現。 如果我看一個函數,看它的簽名沒用,還得去讀它的實現,這不是扯淡麼!
context的鎖爭用
context是一層一層往下傳的,如果全域都是使用同一個傳遞下來的context,會出現一個問題:鎖爭用。
select { case <-context.Done():}
大家都在同一個對象上面調用的Done函數,channel操作最終會加鎖。這個是在etcd項目裡面發現的一個問題,他們改了我們也跟著改了。在起goroutine的時候,一般不要用原來的context了,而是建立一個context,原始的context作為父context。這樣不同goroutine就不會搶同一個鎖。
一般是用的context.WitCancel()這個函數:
go func() { ctx, cancel = context.WithCancel(ctx) doSomething(ctx) cancel()}
調用WithCancel的時候,會得到一個新的子context,以及一個cancel函數。子ctx會在父context的Done函數收到訊號,或者cancel被調用的情況下收到Done訊號。
cancel是需要調用,它使得context釋放相應的資源。開頭提到的bug,就是這個地方被坑到了:這樣寫代碼之後其實有一個假定的約束,即doSomething操作是一個同步的,當它返回以後,相應的context就已經結束了。
然後,我們的代碼在doSomething裡面函數調了很深之後(a調b,b調c,c調d),裡面有一個開goroutine非同步做的操作,於是就傻逼了。那個非同步操作還沒完成,就被cancel掉了。
但是這個問題非常難查,為什嗎?因為單獨看兩個地方的代碼片斷,都沒有看出任何問題。上面那段代碼寫的沒問題呀,只要doSomething是一個同步操作就行。而看doSomething的邏輯也沒問題,它調了其它函數,其它函數繼續調更深的函數,只是到了那裡,並沒有任何關于禁止非同步作業的約束說明。
不要將任何context儲存為成員變數
context的標準用法就是每次都產生一個,然後一層一層往下傳。注意,禁止將context捕獲了儲存下來。不要將任何context儲存為成員變數,不要重用它們。
比如,我要做一個sender對象,它有一個Send方法。那麼我不能在new的時候把ctx儲存下來,在Send的時候使用:
func NewSender(ctx context.Context) *sender { return &sender { ctx: ctx, }}func(s *sender) Send() { grpc.XXX(s.ctx)}
如果調用某個庫它需要傳一個context,你應該給它當時的上下文,如果沒有,可以傳context.Background(),但是不要像上面那樣,建立對象的時候把context儲存下來,到對象的方法調用的時候使用。
正確的使用姿勢不應該看到context被儲存到任何成員變數裡面。
context的作為本質上是動態範圍
上面說到不要將context儲存。讓我們看一看問題的本質:
obj = new Object(ctx)obj.method(ctx)
請問這是同一個上下文嗎? No! 一個時建立時的上下文,一個是運行時的上下文。其實正確來寫,它們是這樣子的:
obj = new Object(ctx1)obj.method(ctx2)
那麼把ctx1儲存下來,給到ctx2用,當然不對。
被坑幾次之後會覺得context很難用。我想了一下,其實這個問題跟動態範圍很類似。現代主流程式設計語言裡面,沒有任何一個採用動態範圍的,而人們大多習慣了詞法範圍,所以思維上很難接受。
正好說一下動態範圍:
func f() { a := 3 func g() int { return a }}
採用詞法範圍的語言,無論在哪裡調用g(),返回的結果都是3。而採用動態範圍的語言,行為完全無法推斷:
a := 7g() // 這裡返回的是7,a的值是看運行時綁定的,而不是聲明時a := 3g() // 這裡返回的是3
當你看到函數需要的參數是一個context,可以context是在每次運行時都不同了,僅僅看聲明並沒有什麼資訊,是不是很像動態範圍?