context 應用場景
Go 的 context 包,可以在我們需要在完成一項工作,會用到多個 routine (完成子任務)時,提供一種方便的在多 routine 間控制(取消、超時等)和傳遞一些跟任務相關的信息的編程方法。
- 一項任務會啓動多個 routine 完成。
- 需要控制和同步多個 routine 的操作。
- 鏈式的在啓動的 routine 時傳遞和任務相關的一些可選信息。
舉一個例子,這裏我們提供了一個服務,每一次調用,它提供三個功能:吃飯、睡覺和打豆豆。調用者通過設置各種參數控制3個操作的時間或次數,同時開始執行這些操作,並且可以在執行過程中隨時終止。
首先,我們定義一下吃飯睡覺打豆豆服務的數據結構。
// DSB represents dining, sleeping and beating Doudou and
// parameters associated with such behaviours.
type DSB struct {
ateAmount int
sleptDuration time.Duration
beatSec int
}
然後提供一個 Do 函數執行我們設置的操作。
func (dsb *DSB) Do(ctx context.Context) {
go dsb.Dining(ctx)
go dsb.Sleep(ctx)
// Limit beating for 3 seconds to prevent a serious hurt on Doudou.
beatCtx, cancelF := context.WithTimeout(ctx, time.Second*3)
defer cancelF()
go dsb.BeatDoudou(beatCtx)
// ...
}
具體的執行某一個操作的方法大概是這樣的:會每隔1秒執行一次,直至完成或者被 cancel。
func (dsb *DSB) BeatDoudou(ctx context.Context) {
for i := 0; i < dsb.beatSec; i++ {
select {
case <-ctx.Done():
fmt.Println("Beating cancelled.")
return
case <-time.After(time.Second * 1):
fmt.Printf("Beat Doudou [%d/%d].\n", i+1, dsb.beatSec)
}
}
}
初始化參數,注意打豆豆的時間會因為我們之前的context.WithTimeout(ctx, time.Second*3)被強制設置為最多3秒。
dsb := DSB{
ateAmount: 5,
sleptDuration: time.Second * 3,
beatSec: 100,
}
ctx, cancel := context.WithCancel(context.Background())
代碼詳見附件。如果順利的執行完,大概是這樣的:
Ate: [0/5].
Beat Doudou [1/100].
Beat Doudou [2/100].
Ate: [1/5].
Beating cancelled.
Have a nice sleep.
Ate: [2/5].
Ate: [3/5].
Ate: [4/5].
Dining completed.
quit
但是如果中途我們發送嘗試終止(發送 SIGINT)的話,會使用 ctx把未執行 完成的行為終止掉。
Ate: [0/5].
Beat Doudou [1/100].
Beat Doudou [2/100].
Ate: [1/5].
^CCancel by user.
Dining cancelled, ate: [2/5].
Sleeping cancelled, slept: 2.95261025s.
Beating cancelled.
quit
推薦的使用方式
- 規則1: 儘量小的 scope。
每個請求類似時候 用法通過簡單的,每次調用時傳入 context 可以明確的定義它對應的調用的取消、截止以及 metadata 的含義,也清晰地做了邊界隔離。要把 context 的 scope 做的越小越好。 - 規則2: 不把 context.Value 當做通用的動態(可選)參數傳遞信息。
在 context 中包含的信息,只能夠是用於描述請求(任務) 相關的,需要在 goroutine 或者 API 之間傳遞(共享)的數據。
通常來説,這種信息可以是 id 類型的(例如玩家id、請求 id等)或者在一個請求或者任務生存期相關的(例如 ip、授權 token 等)。
我們需要澄清,context 的核心功能是==跨 goroutine 操作==。
Go 裏面的 context 是一個非常特別的概念,它在別的語言中沒有等價對象。同時,context 兼具「控制」和「動態參數傳遞」的特性又使得它非常容易被誤用。
Cancel 操作的規則:調用 WithCancel 生成新的 context 拷貝的 routine 可以 Cancel 它的子 routine(通過調用 WithCancel 返回的 cancel 函數),但是一個子 routine 是不能夠通過調用例如 ctx.Cancel()去影響 parsent routine 裏面的行為。
錯誤的用法
不要把 context.Context 保存到其他的數據結構裏。
參考 Contexts and structs
如果把 context 作為成員變量在某一個 struct 中,並且在不同的方法中使用,就混淆了作用域和生存週期。於是使用者無法給出每一次 Cancel 或者 Deadline 的具體意義。對於每一個 context,我們一定要給他一個非常明確的作用域和生存週期的定義。
在下面的這個例子裏面,Server 上面的 ctx 沒有明確的意義。
- 它是用來描述定義
啓動(Serve)服務器的生命週期的? - 它是對
callA/callB引入的 goroutine 的執行的控制? - 它應該在那個地方初始化?
這些都是問題。
type Server struct {
ctx context.Context
// ...
}
func (s *Server) Serve() {
for {
select {
case <-s.ctx.Done():
// ...
}
}
}
func (s *Server) callA() {
newCtx, cancelF := WithCancel(s.ctx)
go s.someCall(newCtx)
// ...
}
func (s *Server) callB() {
// do something
select {
case <-s.ctx.Done():
// ...
case <-time.After(time.Second * 10):
// ...
}
}
例外
有一種允許你把 context 以成員變量的方式使用的場景:兼容舊代碼。
// 原來的方法
func (c *Client) Do(req *Request) (*Response, error)
// 正確的方法定義
func (c *Client) Do(ctx context.Context, req *Request) (*Response, error)
// 為了保持兼容性,原來的方法在不改變參數定義的情況下,把 context 放到 Request 上。
type Request struct {
ctx context.Context
// ...
}
// 創建 Request 時加一個 context 上去。
func NewRequest(method, url string, body io.Reader) (*Request, error) {
return NewRequestWithContext(context.Background(), method, url, body)
}
在上面的代碼中,一個 Request 的請求的嚐盡,是非常契合 context 的設計目的的。因此,在 Client.Do 裏面傳遞 context.Context 是非常符合 Go 的規範且優雅的。
看是考慮到net/http等標準庫已經在大範圍的使用,粗暴的改動接口也是不可取的,因此在net/http/request.go這個文件的實現中,就直接把 ctx 掛在 Request 對象上了。
type Request struct {
// ctx is either the client or server context. It should only
// be modified via copying the whole Request using WithContext.
// It is unexported to prevent people from using Context wrong
// and mutating the contexts held by callers of the same request.
ctx context.Context
在 context 出現前的取消操作
那麼,在沒有 context 的時候,又是如何實現類似取消操作的呢?
我們可以在 Go 1.3 的源碼中瞥見:
// go1.3.3 代碼: go/src/net/http/request.go
// Cancel is an optional channel whose closure indicates that the client
// request should be regarded as canceled. Not all implementations of
// RoundTripper may support Cancel.
//
// For server requests, this field is not applicable.
Cancel <-chan struct{}
使用的時候,把你自己的 chan 設置到 Cancel 字段,並且在你想要 Cancel 的時候 close 那個 chan。
ch := make(chan struct{})
req.Cancel = ch
go func() {
time.Sleep(1 * time.Second)
close(ch)
}()
res, err := c.Do(req)
這種用法看起來有些詭異,我也沒有看到過人這麼使用過。
額外
如果 對一個已經設置了 timeout A 時間的 ctx 再次調用 context.WithTimeout(ctx, timeoutB),得到的 ctx 會在什麼時候超時呢?
答案: timeout A 和 timeout B 中先超時的那個。
附:打豆豆代碼
package main
import (
"context"
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
// DSB represents dining, sleeping and beating Doudou and
// parameters associated with such behaviours.
type DSB struct {
ateAmount int
sleptDuration time.Duration
beatSec int
}
func (dsb *DSB) Do(ctx context.Context) {
wg := sync.WaitGroup{}
wg.Add(3)
go func() {
dsb.Dining(ctx)
wg.Done()
}()
go func() {
dsb.Sleep(ctx)
wg.Done()
}()
// Limit beating for 3 seconds to prevent a serious hurt on Doudou.
beatCtx, cancelF := context.WithTimeout(ctx, time.Second*3)
defer cancelF()
go func() {
dsb.BeatDoudou(beatCtx)
wg.Done()
}()
wg.Wait()
fmt.Println("quit")
}
func (dsb *DSB) Sleep(ctx context.Context) {
begin := time.Now()
select {
case <-ctx.Done():
fmt.Printf("Sleeping cancelled, slept: %v.\n", time.Since(begin))
return
case <-time.After(dsb.sleptDuration):
}
fmt.Printf("Have a nice sleep.\n")
}
func (dsb *DSB) Dining(ctx context.Context) {
for i := 0; i < dsb.ateAmount; i++ {
select {
case <-ctx.Done():
fmt.Printf("Dining cancelled, ate: [%d/%d].\n", i, dsb.ateAmount)
return
case <-time.After(time.Second * 1):
fmt.Printf("Ate: [%d/%d].\n", i, dsb.ateAmount)
}
}
fmt.Println("Dining completed.")
}
func (dsb *DSB) BeatDoudou(ctx context.Context) {
for i := 0; i < dsb.beatSec; i++ {
select {
case <-ctx.Done():
fmt.Println("Beating cancelled.")
return
case <-time.After(time.Second * 1):
fmt.Printf("Beat Doudou [%d/%d].\n", i+1, dsb.beatSec)
}
}
}
func main() {
dsb := DSB{
ateAmount: 5,
sleptDuration: time.Second * 3,
beatSec: 100,
}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{}, 1)
go func() {
dsb.Do(ctx)
done <- struct{}{}
}()
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
select {
case <-sig:
fmt.Println("Cancel by user.")
cancel()
<-done
case <-done:
}
}