前言
Go1.7引入了context
包,其中定義了多種上下文,包括可主動取消的上下文,帶截止時間或超時時間的上下文,帶值傳播的上下文
context
包的引入意義非凡, 它可以讓信息(如:用戶信息)在協程之間傳遞變得更加便捷,也可以把控一組協程的退出時機
假設,當前有一個函數gen
,它接受context
入參,作用是創建一個無緩沖區的channel
,起一個協程
每秒往這個通道中塞入一個信號,當context結束時,關閉通道,協程正常退出
func gen(ctx context.Context) chan struct{} {
c := make(chan struct{})
go func() {
for {
select {
case <-ctx.Done(): // [1]context結束
fmt.Println("receive context done signal, closing channel now")
close(c) // 關閉通道
return // groutine正常退出
default:
c <- struct{}{} // [2]使用空結構體節省內存
}
time.Sleep(time.Second)
}
}()
return c
}
接下來,通過幾個簡單的例子來體驗一下context包提供的各種類型的上下文
例子1:可主動取消的上下文
通過context
包WithCancel
函數,可以創建一個支持主動取消的上下文
func UseCancelContext() {
ctx, cancelFunc := context.WithCancel(context.Background())
go func() {
time.AfterFunc(time.Second*5, func() {
// [1] 五秒后執行取消上下文的操作
cancelFunc()
fmt.Println("send context done signal")
})
}()
for range gen(ctx) {
fmt.Println("receive signal from another goroutine")
}
fmt.Println("main goroutine is finished")
}
[1]處主動調用cancelFunc
,其底層邏輯就是關閉當前上下文及其子上下文中的通道,當上下文中的通道被關閉時,<-ctx.Done()
就會有值,gen
函數中的select
會選擇case <-ctx.Done()
,然后執行關閉通道,退出協程
函數執行結果如下,可以看到,gen函數中的協程在發送了五次信號后,關閉了通道,正常退出協程
<img src="https://images-1255831004.cos.ap-guangzhou.myqcloud.com/online/2021-07-05-132126.png" alt="image-20210705212126405" style="zoom:50%;" />
例子2:帶截止時間的上下文
通過context
包WithDeadline
函數,可以創建一個帶截止時間的上下文
func UseDeadlineContext() {
// 創建一個截止時間為五秒后的上下文
ctx, cancelFunc := context.WithDeadline(context.Background(), time.Now().Add(time.Second*5))
defer cancelFunc() // [1]請思考一下為什么要這樣做?
for range gen(ctx) {
fmt.Println("receive signal from another goroutine")
}
fmt.Println("main goroutine is finished")
}
上述函數創建了一個截止時間為五秒后的上下文,該上下文會在五秒后關閉通道
<img src="https://images-1255831004.cos.ap-guangzhou.myqcloud.com/online/2021-07-05-133503.png" alt="image-20210705213503229" style="zoom:50%;" />
例子3:帶超時時間的上下文
通過context
包WithTimeout
函數,可以創建一個帶超時時間的上下文
func UseTimeoutContext() {
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5)
defer cancelFunc()
for range gen(ctx) {
fmt.Println("receive signal from another goroutine")
}
fmt.Println("main goroutine is finished")
}
上述函數中創建了一個帶五秒超時時間的上下文,代碼執行結果
<img src="https://images-1255831004.cos.ap-guangzhou.myqcloud.com/online/2021-07-05-133737.png" alt="image-20210705213737346" style="zoom:50%;" />
例子4:帶值傳播的上下文
通過context
包WithValue
函數,可以創建一個帶值傳播的上下文
func UseValueContext() {
// 創建一個帶值的上下文,鍵值對<1,"value">
ctx := context.WithValue(context.Background(), 1, "value")
c := make(chan struct{})
go func() {
time.Sleep(time.Second * 1)
// 從上下文中獲取對應的值
value := ctx.Value(1)
fmt.Printf("get value from ctx, value = %s\n", value)
c <- struct{}{}
}()
<-c
fmt.Println("main goroutine is finished")
}
執行結果
<img src="https://images-1255831004.cos.ap-guangzhou.myqcloud.com/online/2021-07-05-134207.png" alt="image-20210705214206616" style="zoom:50%;" />
總結
日常開發中需要使用context
包的場景非常多,簡單舉幾個例子:
- 從請求上下文中獲取用戶信息
- 從請求上下文中獲取請求的唯一標識(
traceId
,常用于分布式日志追蹤) - 控制請求超時時間
可以看到,通過context
包,我們更容易實現信息在一組協程之間的傳播,也更好地把控多個協程的退出時機
Go也重寫了很多標準庫來支持context包
同時Go官方建議上下文不應該嵌入到結構體中使用,而是應該將上下文作為調用函數/方法的第一個參數使用,并推薦參數命名為ctx
// 不推薦
type ModuleContext struct {
c context.Context
}
// 推薦
func func1(ctx context.Context, opts ...Option)
掌握這個包的使用和了解其中原理是很有必要的
下一篇文章我將會解答在這篇文章中的留下的疑問(例子2
[1]
處),以及進一步挖掘context
包源碼