go的context問題

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

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是在每次運行時都不同了,僅僅看聲明並沒有什麼資訊,是不是很像動態範圍?

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.