我所知道的 Golang Context

前言

最近在做一個 Side Project 。
使用到了 MongoDB,自然而然地去找了 Mongodb 的 Driver 來幫助開發。
看了範例之後,常常看到 context 的使用,比如說

ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
res, err := collection.InsertOne(ctx, bson.D{{"name", "pi"}, {"value", 3.14159}})
id := res.InsertedID

雖然對 context 有一定的概念(提供 timeout, cancel 機制),卻沒有好好的統整一下 Golang 的 context 到底在做些什麼?

就讓我來說說,我現在所知道的 Golang Context 吧!

Context 的主要功用

  1. Context 訊息傳遞(request-scoped),處理 http 請求
  2. 控制子 goroutine 的運行
  3. timeout
  4. cancellation

Context 基本解析

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <- struct{}
    Err() error
    Value(key interface{}) interface{}
}
  1. Dealine():會回傳 Context 被取消的時間,如果沒有 ok 會回傳 false。
  2. Done():會讓你拿到一個 channel。如果 context 被取消,channel 會被 close ,需要從 Err() 去取得取消錯誤訊息
  3. Err():人如其名
  4. Value:人如其名,就是該 context 想傳遞的資料

我們常用的 Context 物件:

  1. context.Background():常用在 main func、初始化…
  2. context.TODO():當你不知道該用什麼的時候可以用(原文:it’s unclear which Context to use)

無腦使用 context.TODO()

但老實說,他們從 source code 看起來像是一樣的東西

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)

func Background() Context {
    return background
}

func TODO() Context {
    return todo
}

Context 的一些常見使用規範

  1. 如果有使用 Context 通常會把 Context 當作 function 的第一個參數
  2. 如果有使用 Context 寧願傳一個空的 Context (contex.TODO()),也不要傳 nil
  3. 不要持久化 Context ,Context 應該就代表一個時間內的狀態。
  4. Value(key interface{}),內的 key 盡量使用自定義的類型。

Context 的幾種用法

WithValue

ctx := context.TODO()
ctx = context.WithValue(ctx, "key1", "value1")
ctx = context.WithValue(ctx, "key2", "value2")
ctx = context.WithValue(ctx, "key3", "value3")
ctx = context.WithValue(ctx, "key4", "value4")

fmt.Println(ctx.Value("key1")) // print value1

可以看到當我們使用 ctx.Value("key") 時,他其實是會一直往上去找的。 會有這樣的情況是 WithContext 方法是回傳了一個 &valueCtx 的 struct 。

type valueCtx struct {
    Context
    key, val interface{}
}

當在尋找 Value 時,如果發現 parent 是 valueCtx 的話,他就會一路向上。直到 praent 是 Context

WithCancel

func operation1(ctx context.Context) error {
	time.Sleep(100 * time.Millisecond)
	return errors.New("failed")
}

func operation2(ctx context.Context) {
	select {
	case <-time.After(500 * time.Millisecond):
		fmt.Println("done")
	case <-ctx.Done():
		fmt.Println("halted operation2")
	}
}

func main() {
	ctx := context.Background()
	ctx, cancel := context.WithCancel(ctx)

	go func() {
		err := operation1(ctx)
		if err != nil {
			cancel()
		}
	}()
	operation2(ctx)
}

從上面的例子可以看到 WithCancel 的基本用法。
WithCanel 的 cancel function 被呼叫的同時,其他 Goroutine 可以透過 Done() 來知道該 context 被取消了,可以做其他相關的作業。

Cancel function 不只在你想要 cancel 時才使用,任務正常結束也要執行,這樣才能順利釋放 context,所以一般建議配合 defer 一起使用。

WithDeadline && WithTimeout

先來看看官方的使用範例

func main() {
	d := time.Now().Add(shortDuration)
	ctx, cancel := context.WithDeadline(context.Background(), d)

	// Even though ctx will be expired, it is good practice to call its
	// cancellation function in any case. Failure to do so may keep the
	// context and its parent alive longer than necessary.
	defer cancel()

	select {
	case <-time.After(1 * time.Second):
		fmt.Println("overslept")
	case <-ctx.Done():
		fmt.Println(ctx.Err())
	}

}

這兩個會放在一起說的原因是基本上是相同的東西

// WithTimeout
func Withtimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

// WithDeadline
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	if cur, ok := parent.Deadline(); ok && cur.Before(d) {
		// The current deadline is already sooner than the new one.
		return WithCancel(parent)
	}
	c := &timerCtx{
		cancelCtx: newCancelCtx(parent),
		deadline:  d,
	}
	propagateCancel(parent, c)
	dur := time.Until(d)
	if dur <= 0 {
		c.cancel(true, DeadlineExceeded) // deadline has already passed
		return c, func() { c.cancel(false, Canceled) }
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err == nil {
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func() { c.cancel(true, Canceled) }
}

總結一下 timerCtx 的 Done 會被 Close 的情況為:

  1. 截止時間到了
  2. cancel function 被調用了
  3. parent 的 Done 被 close

所以我們可以透過 Done() 來知道這個 Context 是否被取消了。

這裡也要特別注意,不要依賴 Deadline Or Timeout,如果工作早早完成也可以及早 cancel context。

小結

以上就是我現在所知道的 Golang Context。如果有錯誤或是其他指教在煩請留言來信告知。

資料來源

Using Context in Golang - Cancellation, Timeouts and Values (With Examples)

極客時間 - Go 併發編程實戰課