天天看點

golang context的深度了解

作者:幹飯人小羽
golang context的深度了解

1. 什麼是 context

Go 1.7 标準庫引入 context,中文譯作“上下文”,準确說它是 goroutine 的上下文,包含 goroutine 的運作狀态、環境、現場等資訊。

context 主要用來在 goroutine 之間傳遞上下文資訊。

随着 context 包的引入,标準庫中很多接口是以加上了 context 參數。context 幾乎成為了并發控制和逾時控制的标準做法。

context.Context 類型的值可以協調多個 groutine 中的代碼執行“取消”操作,并且可以存儲鍵值對。最重要的是它是并發安全的。

2. 為什麼有 context

Go 常用來寫背景服務,通常隻需要幾行代碼,就可以搭建一個 http server。

在 Go 的 server 裡,通常每來一個請求都會啟動若幹個 goroutine 同時工作:有些去資料庫拿資料,有些調用下遊接口擷取相關資料……

golang context的深度了解

這些 goroutine 需要共享這個請求的基本資料,例如登陸的 token,處理請求的最大逾時時間等等。當請求被取消或是處理時間太長,這時所有正在為這個請求工作的 goroutine 需要快速退出。相關聯的 goroutine 都退出後,系統就可以回收相關的資源。

Go 語言中的 server 實際上是一個“協程模型”,即一個協程處理一個請求。例如在業務高峰期,某個下遊服務的響應變慢,而目前系統的請求又沒有逾時控制,或者逾時時間設定地過大,那麼等待下遊服務傳回資料的協程就會越來越多。而後果就是協程數激增,記憶體占用飙漲,甚至導緻服務不可用。更嚴重的會導緻雪崩效應,整個服務對外表現為不可用。

其實前面描述的事故,通過設定 允許下遊最長處理時間 就可以避免。例如,給下遊設定的 timeout 是 50 ms,如果超過這個值還沒有接收到傳回資料,就直接向用戶端傳回一個預設值或者錯誤。

context 包就是為了解決上面所說的這些問題而開發的:在 一組 goroutine 之間傳遞共享的值、取消信号、deadline……

golang context的深度了解

在Go 裡,我們不能直接殺死協程,協程的關閉一般會用 channel+select 方式來控制。但是在某些場景下,例如處理一個請求衍生了很多協程,這些協程之間是互相關聯的:需要共享一些全局變量、有共同的 deadline 等,而且可以同時被關閉。再用 channel+select 就會比較麻煩,這時就可以通過 context 來實作。

一句話:context 用來解決 goroutine 之間退出通知、中繼資料傳遞的功能。

3. context 底層實作原理

3.1 整體概覽

類型 名稱 作用
Context 接口 定義了 Context 接口的四個方法
emptyCtx 結構體 實作了 Context 接口,它其實是個空的 context
CancelFunc 函數 取消函數
canceler 接口 context 取消接口,定義了兩個方法
cancelCtx 結構體 可以被取消
timerCtx 結構體 逾時會被取消
valueCtx 結構體 可以存儲 k-v 對
Background 函數 傳回一個空的 context,常作為根 context
TODO 函數 傳回一個空的 context,常用于重構時期,沒有合适的 context 可用
WithCancel 函數 基于父 context,生成一個可以取消的 context
newCancelCtx 函數 建立一個可取消的 context
propagateCancel 函數 向下傳遞 context 節點間的取消關系
parentCancelCtx 函數 找到第一個可取消的父節點
removeChild 函數 去掉父節點的孩子節點
WithDeadline 函數 建立一個有 deadline 的 context
WithTimeout 函數 建立一個有 timeout 的 context
WithValue 函數 建立一個存儲 k-v 對的 context

上面這張表展示了 context 的所有函數、接口、結構體,可以縱覽全局,可以在讀完文章後,再回頭細看。

整體類圖如下:

golang context的深度了解

3.2 接口

3.2.1 Context

scss複制代碼type Context interface {
	// 當 context 被取消或者到了 deadline,傳回一個被關閉的 channel
	Done() <-chan struct{}

	// 在 channel Done 關閉後,傳回 context 取消原因
	Err() error

	// 傳回 context 是否會被取消以及自動取消時間(即 deadline)
	Deadline() (deadline time.Time, ok bool)

	// 擷取 key 對應的 value
	Value(key interface{}) interface{}
}
           

Context 是一個接口,定義了 4 個方法,它們都是幂等的。即連續多次調用同一個方法,得到的結果都是相同的。

Done() 傳回一個 channel,可以表示 context 被取消的信号:當這個 channel 被關閉時,說明 context 被取消了。注意,這是一個隻讀的channel。 我們又知道,讀一個關閉的 channel 會讀出相應類型的零值。并且源碼裡沒有地方會向這個 channel 裡面塞入值。換句話說,這是一個 receive-only 的 channel。是以在子協程裡讀這個 channel,除非被關閉,否則讀不出來任何東西。也正是利用了這一點,子協程從 channel 裡讀出了值(零值)後,就可以做一些收尾工作,盡快退出。

Err() 傳回一個錯誤,表示 channel 被關閉的原因。例如是被取消,還是逾時。

Deadline() 傳回 context 的截止時間。

Value() 擷取之前設定的 key 對應的 value。

3.2.2 canceler

go複制代碼type canceler interface {
	cancel(removeFromParent bool, err error)
	Done() <-chan struct{}
}
           

實作上面定義的兩個方法的 Context,表明該 Context 是可取消的。源碼中有兩個類型實作了 canceler 接口:*cancelCtx 和 *timerCtx。注意是加了 * 号的,是這兩個結構體的指針實作了 canceler 接口。

Context 接口設計成這個樣子的原因:

  • “取消”操作應該是建議性,而非強制性
  • “取消”操作應該可傳遞

    “取消”某個函數時,和它相關聯的其他函數也應該“取消”。是以,Done() 方法傳回一個隻讀的 channel,所有相關函數監聽此 channel。一旦 channel 關閉,通過 channel 的“廣播機制”,所有監聽者都能收到。

3.3 結構體

3.3.1 emptyCtx

go複制代碼type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
	return
}

func (*emptyCtx) Done() <-chan struct{} {
	return nil
}

func (*emptyCtx) Err() error {
	return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {
	return nil
}
           

一個空的 context,永遠不會被 cancel,沒有存儲值,也沒有 deadline。

它被包裝成:

go複制代碼var (
	background = new(emptyCtx)
	todo       = new(emptyCtx)
)
           

通過下面兩個導出的函數對外公開:

go複制代碼func Background() Context {
	return background
}

func TODO() Context {
	return todo
}
           

background 通常用在 main 函數中,作為所有 context 的根節點。

todo 通常用在并不知道傳遞什麼 context的情形。例如,調用一個需要傳遞 context 參數的函數,你手頭并沒有其他 context 可以傳遞,這時就可以傳遞 todo。這常常發生在重構進行中,給一些函數添加了一個 Context 參數,但不知道要傳什麼,就用 todo “占個位子”,最終要換成其他 context。

3.3.2 cancelCtx

go複制代碼type cancelCtx struct {
	Context

	// 保護之後的字段
	mu       sync.Mutex
	done     chan struct{}
	children map[canceler]struct{}
	err      error
}
           

可取消的 Context,實作了 canceler 接口。它直接将接口 Context 作為它的一個匿名字段,這樣就可以被看成一個 Context。

Done() 方法的實作:

go複制代碼func (c *cancelCtx) Done() <-chan struct{} {
	c.mu.Lock()
	if c.done == nil {
		c.done = make(chan struct{})
	}
	d := c.done
	c.mu.Unlock()
	return d
}
           

c.done 是“懶漢式”建立,隻有調用了 Done() 方法的時候才會被建立。再次說明,函數傳回的是一個隻讀的 channel,而且沒有地方向這個 channel 裡面寫資料。是以,直接調用讀這個 channel,協程會被 block 住。一般通過搭配 select 來使用。一旦關閉,就會立即讀出零值。

Err() 和 String() 方法比較簡單,不多說。

重點關注 cancel() 方法的實作:

go複制代碼func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    // 必須要傳 err
	if err == nil {
		panic("context: internal error: missing cancel error")
	}
	c.mu.Lock()
	if c.err != nil {
		c.mu.Unlock()
		return // 已經被其他協程取消
	}
	// 給 err 字段指派
	c.err = err
	// 關閉 channel,通知其他協程
	if c.done == nil {
		c.done = closedchan
	} else {
		close(c.done)
	}
	
	// 周遊它的所有子節點
	for child := range c.children {
	    // 遞歸地取消所有子節點
		child.cancel(false, err)
	}
	// 将子節點置空
	c.children = nil
	c.mu.Unlock()

	if removeFromParent {
	    // 從父節點中移除自己 
		removeChild(c.Context, c)
	}
}
           

cancel() 的功能就是關閉 channel:c.done;遞歸地取消它的所有子節點;從父節點從删除自己。達到的效果是通過關閉 channel,将取消信号傳遞給了它的所有子節點。goroutine 接收到取消信号的方式就是 select 語句中的讀 c.done 被選中。

建立一個可取消的 Context 的方法:

go複制代碼func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}

func newCancelCtx(parent Context) cancelCtx {
	return cancelCtx{Context: parent}
}
           

這是一個暴露給使用者的方法,傳入一個父 Context(通常是 background,作為根節點),傳回建立的 context,新 context 的 done channel 是建立的。

當 WithCancel 函數傳回的 CancelFunc 被調用或者是父節點的 done channel 被關閉(父節點的 CancelFunc 被調用),此 context(子節點) 的 done channel 也會被關閉。

注意傳給 c.cancel 方法的參數,前者是 true,也就是說取消的時候,需要将自己從父節點裡删除。第二個參數則是一個固定的取消錯誤類型:

ini複制代碼var Canceled = errors.New("context canceled")
           

還注意到一點,調用子節點 cancel 方法的時候,傳入的第一個參數 removeFromParent 是 false。

兩個問題需要回答:1. 什麼時候會傳 true?2. 為什麼有時傳 true,有時傳 false?

當 removeFromParent 為 true 時,會将目前節點的 context 從父節點 context 中删除:

css複制代碼func removeChild(parent Context, child canceler) {
	p, ok := parentCancelCtx(parent)
	if !ok {
		return
	}
	p.mu.Lock()
	if p.children != nil {
		delete(p.children, child)
	}
	p.mu.Unlock()
}
           

最關鍵的一行:

scss複制代碼delete(p.children, child)
           

在取消函數内部,所有子節點都會因為c.children = nil 而化為灰燼。自然沒必要再多做這一步(傳true)。另外,如果周遊子節點時,調用 child.cancel 函數傳了 true,還會造成同時周遊和删除一個 map 的境地,會有問題的。

golang context的深度了解

重點看 propagateCancel():

go複制代碼func propagateCancel(parent Context, child canceler) {
	// 父節點是個空節點
	if parent.Done() == nil {
		return 
	}
	// 找到可以取消的父 context
	if p, ok := parentCancelCtx(parent); ok {
		p.mu.Lock()
		if p.err != nil {
			// 父節點已經被取消了,本節點(子節點)也要取消
			child.cancel(false, p.err)
		} else {
			// 父節點未取消
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			// "挂到"父節點上
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
		// 如果沒有找到可取消的父 context。新啟動一個協程監控父節點或子節點取消信号
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}
           

這個方法的作用就是向上尋找可以“挂靠”的“可取消”的 context,并且“挂靠”上去。這樣,調用上層 cancel 方法的時候,就可以層層傳遞,将那些挂靠的子 context 同時“取消”。

這裡着重解釋下為什麼會有 else 描述的情況發生。else 是指目前節點 context 沒有向上找到可以取消的父節點,那麼就要再啟動一個協程監控父節點或者子節點的取消動作。

這裡就有疑問了,既然沒找到可以取消的父節點,那 case <-parent.Done() 這個 case 就永遠不會發生,是以可以忽略這個 case;而 case <-child.Done() 這個 case 又啥事不幹。那這個 else 不就多餘了嗎?

其實不然。我們來看 parentCancelCtx 的代碼:

go複制代碼func parentCancelCtx(parent Context) (*cancelCtx, bool) {
	for {
		switch c := parent.(type) {
		case *cancelCtx:
			return c, true
		case *timerCtx:
			return &c.cancelCtx, true
		case *valueCtx:
			parent = c.Context
		default:
			return nil, false
		}
	}
}
           

這裡隻會識别三種 Context 類型:*cancelCtx,*timerCtx,*valueCtx。若是把 Context 内嵌到一個類型裡,就識别不出來了。

驗證上面的說法:

go複制代碼type MyContext struct {
	Context
}

func main() {
	childCancel := true

	parentCtx, parentFunc := WithCancel(Background())
	mctx := MyContext{parentCtx}

	childCtx, childFun := WithCancel(mctx)

	fmt.Println(parentCtx)
	fmt.Println(mctx)
	fmt.Println(childCtx)
}
           

我們看下三個 context 的列印結果:

複制代碼context.Background.WithCancel
{context.Background.WithCancel}
{context.Background.WithCancel}.WithCancel
           

果然,mctx,childCtx 和正常的 parentCtx 不一樣,因為它是一個自定義的結構體類型。

else 這段代碼(propagateCancel()函數裡第二個else)說明,如果把 ctx 強行塞進一個結構體,并用它作為父節點,調用 WithCancel 函數建構子節點 context 的時候,Go 會新啟動一個協程來監控取消信号。

再來說一下,select 語句裡的兩個 case 其實都不能删。

vbscript複制代碼select {
	case <-parent.Done():
		child.cancel(false, parent.Err())
	case <-child.Done():
}
           

第一個 case 說明當父節點取消,則取消子節點。如果去掉這個 case,那麼父節點取消的信号就不能傳遞到子節點。

第二個 case 是說如果子節點自己取消了,那就退出這個 select,父節點的取消信号就不用管了。如果去掉這個 case,那麼很可能父節點一直不取消,這個 goroutine 就洩漏了。當然,如果父節點取消了,就會重複讓子節點取消,不過,這也沒什麼影響嘛。

3.3.3 timerCtx

timerCtx 基于 cancelCtx,隻是多了一個 time.Timer 和一個 deadline。Timer 會在 deadline 到來時,自動取消 context。

lua複制代碼type timerCtx struct {
	cancelCtx
	timer *time.Timer 

	deadline time.Time
}
           

timerCtx 首先是一個 cancelCtx,是以它能取消。看下 cancel() 方法:

scss複制代碼func (c *timerCtx) cancel(removeFromParent bool, err error) {
	// 直接調用 cancelCtx 的取消方法
	c.cancelCtx.cancel(false, err)
	if removeFromParent {
		// 從父節點中删除子節點
		removeChild(c.cancelCtx.Context, c)
	}
	c.mu.Lock()
	if c.timer != nil {
		// 關掉定時器,這樣,在deadline 到來時,不會再次取消
		c.timer.Stop()
		c.timer = nil
	}
	c.mu.Unlock()
}
           

建立 timerCtx 的方法:

scss複制代碼func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}
           

WithTimeout 函數直接調用了 WithDeadline,傳入的 deadline 是目前時間加上 timeout 的時間,即從現在開始再經過 timeout 時間就算逾時。WithDeadline 需要用的是絕對時間。重點來看它:

scss複制代碼func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
	if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
		// 如果父節點 context 的 deadline 早于指定時間。直接建構一個可取消的 context。
		// 原因是一旦父節點逾時,自動調用 cancel 函數,子節點也會随之取消。
		// 是以不用單獨處理子節點的計時器時間到了之後,自動調用 cancel 函數
		return WithCancel(parent)
	}
	
	// 建構 timerCtx
	c := &timerCtx{
		cancelCtx: newCancelCtx(parent),
		deadline:  deadline,
	}
	// 挂靠到父節點上
	propagateCancel(parent, c)
	
	// 計算目前距離 deadline 的時間
	d := time.Until(deadline)
	if d <= 0 {
		// 直接取消
		c.cancel(true, DeadlineExceeded)
		return c, func() { c.cancel(true, Canceled) }
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err == nil {
		// d 時間後,timer 會自動調用 cancel 函數。自動取消
		c.timer = time.AfterFunc(d, func() {
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func() { c.cancel(true, Canceled) }
}
           

這個函數的最核心的一句是:

go複制代碼c.timer = time.AfterFunc(d, func() {
	c.cancel(true, DeadlineExceeded)
})
           

c.timer 會在 d 時間間隔後,自動調用 cancel 函數,并且傳入的錯誤就是 DeadlineExceeded:

go複制代碼var DeadlineExceeded error = deadlineExceededError{}

type deadlineExceededError struct{}

func (deadlineExceededError) Error() string   { return "context deadline exceeded" }
           

也就是逾時錯誤。

3.3.4 valueCtx

go複制代碼type valueCtx struct {
	Context
	key, val interface{}
}
           

它實作了兩個方法:

vbnet複制代碼func (c *valueCtx) String() string {
	return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}

func (c *valueCtx) Value(key interface{}) interface{} {
	if c.key == key {
		return c.val
	}
	return c.Context.Value(key)
}
           

建立 valueCtx 的函數:

go複制代碼func WithValue(parent Context, key, val interface{}) Context {
	if key == nil {
		panic("nil key")
	}
	if !reflect.TypeOf(key).Comparable() {
		panic("key is not comparable")
	}
	return &valueCtx{parent, key, val}
}
           

對 key 的要求是可比較,因為之後需要通過 key 取出 context 中的值,可比較是必須的。

通過層層傳遞 context,最終形成這樣一棵樹:

golang context的深度了解

取值的過程,實際上是一個遞歸查找的過程:

vbnet複制代碼func (c *valueCtx) Value(key interface{}) interface{} {
	if c.key == key {
		return c.val
	}
	return c.Context.Value(key)
}
           

它會順着鍊路一直往上找,比較目前節點的 key 是否是要找的 key,是則傳回 value。否則,一直順着 context 往前,最終找到根節點,直接傳回一個 nil。是以用 Value 方法的時候要判斷結果是否為 nil。

WithValue 建立 context 節點的過程實際上就是建立連結清單節點的過程。兩個節點的 key 值是可以相等的,但它們是兩個不同的 context 節點。查找的時候,會向上查找到最後一個挂載的 context 節點,即離得比較近的一個父 context。是以,用 WithValue 構造的其實是一個低效率的連結清單。

如果你接手過項目,肯定經曆過這樣的窘境:在一個處理過程中,有若幹子函數、子協程。各種不同的地方會向 context 裡塞入各種不同的 k-v 對,最後在某個地方使用。

你根本就不知道什麼時候什麼地方傳了什麼值?這些值會不會被“覆寫”(底層是兩個不同的 context 節點,查找的時候,隻會傳回一個結果)?你肯定會崩潰的。

而這也是 context.Value 最受争議的地方。很多人建議盡量不要通過 context 傳值。

4. 如何使用 context

context 使用起來非常友善。源碼裡對外提供了一個建立根節點 context 的函數:

go複制代碼func Background() Context
           

background 是一個空的 context, 它不能被取消,沒有值,也沒有逾時時間。

有了根節點 context,又提供了四個函數建立子節點 context:

scss複制代碼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, val interface{}) Context
           

context 會在函數傳遞間傳遞。隻需要在适當的時間調用 cancel 函數向 goroutines 發出取消信号或者調用 Value 函數取出 context 中的值。

在官方部落格裡,對于使用 context 提出了幾點建議:

  1. 不要将 Context 塞到結構體裡。直接将 Context 類型作為函數的第一參數,而且一般都命名為 ctx。
  2. 不要向函數傳入一個 nil 的 context,如果你實在不知道傳什麼,标準庫給你準備好了一個 context:todo。
  3. 不要把本應該作為函數參數的類型塞到 context 中,context 存儲的應該是一些共同的資料。例如:登陸的 session、cookie 等。
  4. 同一個 context 可能會被傳遞到多個 goroutine,别擔心,context 是并發安全的。

4.1 取消 goroutine

我們先來設想一個場景:打開外賣的訂單頁,地圖上顯示外賣小哥的位置,而且是每秒更新 1 次。app 端向背景發起 websocket 連接配接請求後,背景啟動一個協程,每隔 1 秒計算 1 次小哥的位置,并發送給用戶端。如果使用者退出此頁面,則背景需要“取消”此過程,退出 goroutine,系統回收資源。

後端可能的實作如下:

scss複制代碼func Perform() {
    for {
        calculatePos()
        sendResult()
        time.Sleep(time.Second)
    }
}
           

如果需要實作“取消”功能,并且在不了解 context 功能的前提下,可能會這樣做:給函數增加一個指針型的 bool 變量,在 for 語句的開始處判斷 bool 變量是發由 true 變為 false,如果改變,則退出循環。

上面給出的簡單做法,可以實作想要的效果,沒有問題,但是并不優雅,并且一旦協程數量多了之後,并且各種嵌套,就會很麻煩。優雅的做法,自然就要用到 context。

go複制代碼func Perform(ctx context.Context) {
    for {
        calculatePos()
        sendResult()

        select {
        case <-ctx.Done():
            // 被取消,直接傳回
            return
        case <-time.After(time.Second):
            // block 1 秒鐘 
        }
    }
}
           

主流程可能是這樣的:

css複制代碼ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
go Perform(ctx)

// ……
// app 端傳回頁面,調用cancel 函數
cancel()
           

注意一個細節,WithTimeOut 函數傳回的 context 和 cancelFun 是分開的。context 本身并沒有取消函數,這樣做的原因是取消函數隻能由外層函數調用,防止子節點 context 調用取消函數,進而嚴格控制資訊的流向:由父節點 context 流向子節點 context。

4.2 防止 goroutine 洩漏

前面那個例子裡,goroutine 還是會自己執行完,最後傳回,隻不過會多浪費一些系統資源。這裡改編一個“如果不用 context 取消,goroutine 就會洩漏的例子。

go複制代碼func gen() <-chan int {
	ch := make(chan int)
	go func() {
		var n int
		for {
			ch <- n
			n++
			time.Sleep(time.Second)
		}
	}()
	return ch
}
           

這是一個可以生成無限整數的協程,但如果我隻需要它産生的前 5 個數,那麼就會發生 goroutine 洩漏:

csharp複制代碼func main() {
	for n := range gen() {
		fmt.Println(n)
		if n == 5 {
			break
		}
	}
	// ……
}
           

當 n == 5 的時候,直接 break 掉。那麼 gen 函數的協程就會執行無限循環,永遠不會停下來。發生了 goroutine 洩漏。

用 context 改進這個例子:

go複制代碼func gen(ctx context.Context) <-chan int {
	ch := make(chan int)
	go func() {
		var n int
		for {
			select {
			case <-ctx.Done():
				return
			case ch <- n:
				n++
				time.Sleep(time.Second)
			}
		}
	}()
	return ch
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel() // 避免其他地方忘記 cancel,且重複調用不影響

	for n := range gen(ctx) {
		fmt.Println(n)
		if n == 5 {
			cancel()
			break
		}
	}
	// ……
}
           

增加一個 context,在 break 前調用 cancel 函數,取消 goroutine。gen 函數在接收到取消信号後,直接退出,系統回收資源。

5. context 真的這麼好嗎

讀完全文,你一定有這種感覺:context 就是為 server 而設計的。說什麼處理一個請求,需要啟動多個 goroutine 并行地去處理,并且在這些 goroutine 之間還要傳遞一些共享的資料等等,這些都是寫一個 server 要做的事。

Go 官方建議我們把 Context 作為函數的第一個參數,甚至連名字都準備好了。這造成一個後果:因為我們想控制所有的協程的取消動作,是以需要在幾乎所有的函數裡加上一個 Context 參數。很快,我們的代碼裡,context 将像病毒一樣擴散的到處都是。

另外,像 WithCancel、WithDeadline、WithTimeout、WithValue 這些建立函數,實際上是建立了一個個的連結清單結點而已。我們知道,對連結清單的操作,通常都是 O(n) 複雜度的,效率不高。

那麼,context 包到底解決了什麼問題呢?答案是:cancelation。僅管它并不完美,但它确實很簡潔地解決了問題。

總結

context 包是 Go 1.7 引入的标準庫,主要用于在 goroutine 之間傳遞取消信号、逾時時間、截止時間以及一些共享的值等。

使用上,先建立一個根節點的 context,之後根據庫提供的四個函數建立相應功能的子節點 context。由于它是并發安全的,是以可以放心地傳遞。

當使用 context 作為函數參數時,直接把它放在第一個參數的位置,并且命名為 ctx。另外,不要把 context 嵌套在自定義的類型裡。

連結:https://juejin.cn/post/7170520137433350151