Go context

Gary Liao
5 min readJun 7, 2023

--

核心是個 channel

概述

context package 是 Go 1.7 版推出的套件,用於程序間傳遞截止時間 (deadlines)、 取消訊號 (cancellation signals), 以及其他(比方說API查詢時)所帶的值 (request-scoped values)。

這件事情不是不能靠 channel 做到,況且 context package 還是基於 channels 去做到傳遞訊號的功能,(以下是我的推測) 但在官方正式把 context 納入標準庫之前,實踐方法應該是五花八門。 context 的推出,某種程度統一了混亂的局面 (推測結束)。

context 也應用在官方的 http.Request 之中,request 內就包了一個 context,因此 context 現在可說是用 Go 寫後端的標配。

情境

如下圖,A是一系列程序的頭,分支出許多子程序,子程序又有子程序,那該如何從源頭取消整個系列的程序?

     A
/ | \
B C D
/ \ /|\
E F G H I

最簡單的方法是宣告一個 channel 去傳遞取消訊號,再讓所有子程序都聽這個 channel 。

到了 context 的年代原理也是一樣,但由於諸多考量,所以把 channel 包在 context 之中:宣告一個 context,內含一個 channel 去傳遞取消訊號,再將這個 context 作為參數傳入所有子程序,子程序都聽這個 context 夾帶的 channel。

規範

Context必須在整串程序中傳遞下去。

package 說明原文是這樣寫的:

Incoming requests to a server should create a Context, and outgoing calls to servers should accept a Context. The chain of function calls between them must propagate the Context, optionally replacing it with a derived Context created using WithCancel, WithDeadline, WithTimeout, or WithValue. When a Context is canceled, all Contexts derived from it are also canceled.

而且,這個傳遞必須是顯性的,放在第一個參數位置,命名為ctx,不能包在別的 struct 之中。

Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx.

或許有人開始擔心,ctx 的傳遞,如果在 goroutines 很大量的情況下,會不會造成影響?其實不會,因為 ctx 本身是 pointer。

簡單的例子

下面這個例子中,ctx 是 goroutine function 的第一個參數,worker 會倒數,會無限執行,直到 main goroutine 呼叫了 cancel():

package main

import (
"context"
"fmt"
"time"
)

func worker(ctx context.Context, done chan struct{}, i int) {
for range time.Tick(time.Second) {
select {
case <-ctx.Done():
fmt.Println("Canceled by ctx signal.")
fmt.Printf("%T\n", ctx)
done <- struct{}{}
default:
fmt.Println(i)
i -= 1
}
}
fmt.Println("This line will never been executed.")
done <- struct{}{}
}

func main() {
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
CntDown := 3
go worker(ctx, done, CntDown)
time.Sleep(time.Duration(CntDown) * time.Second)
cancel()
<-done
}

結果

3
2
1
Canceled by ctx signal.
*context.cancelCtx

Context 的行為類型

我將 context 的行為分成兩種類型以便理解:他殺、定時。

他殺

這類的 context 會伴隨產生一個 cancel function,用來讓父程序殺死整串子程序。

定時

這類的 context 除了會伴隨產生一個 cancel function,還可先定義大限之日或時限,時間到了,整串子程序就會被殺。常被用於限制 requests 的執行時間。

透過 Context 夾帶 Key Value

假如說有資料要傳遞給子程序,而這資料只活在這串程序中,程序結束就可拋棄,可以透過 context 夾帶。

透過定時改寫上面範例

// 前略

func main() {
CntDown := 3
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(CntDown)*time.Second)
defer cancel()
done := make(chan struct{})

go worker(ctx, done, CntDown)
<-done
}

--

--