Go1.21 速覽:Context 可以設置取消原因和回調(diào)函數(shù)了,等的可太久了!
大家好,我是煎魚。
在 Go 中有一個很經(jīng)典的設計:context,這是許多同學初學時必學的標準庫。涉及到上下文傳遞、超時控制等必要項。
甚至在函數(shù)體中的第一個參數(shù)大多是傳 context。寫第三方庫也必須兼容 context 設置,否則會經(jīng)常有人提需求讓你支持。
我覺得這次的新特性更新雖不復雜,但作用挺大。建議大家學習!
Context Demo
以下是一個快速 Demo:
package main
import (
"context"
"fmt"
"time"
)
const shortDuration = 1 * time.Millisecond
func main() {
ctx, cancel := context.WithTimeout(context.Background(), shortDuration)
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}
運行結果:
context deadline exceeded
一切都看起來沒什么問題。
麻煩點
但在實際寫業(yè)務代碼和排查問題時,你就會發(fā)現(xiàn)一個麻煩的事。在出現(xiàn)上下文超時或到達所設置的截止時間時,ctx.Err 方法可以獲得 context deadline exceeded 的錯誤信息。
但這是遠遠不夠的,你只知道是因為誘發(fā)了超時。但不知道是哪里導致的,還得再去根據(jù)訪問的邏輯,再走一遍腦洞,再進行排查。又或是根據(jù)代碼堆棧,再去設想,最后復現(xiàn)成功。
又或是查不到。因為這種一般是偶現(xiàn),很有可能就留給下一代的繼承者了~
又更有業(yè)務訴求,希望在出現(xiàn)上下文的異常場景時,可以及時執(zhí)行回調(diào)方法。然而這沒有太便捷的實現(xiàn)方式。
Go1.21 增強 Context
增加 WithXXXCause
在即將發(fā)布的 Go1.21,針對 Context 的錯誤處理終于有了一點點的增強,來填補這個地方的信息,允許添加自定義的錯誤類型和信息。
新增的 Context API 如下:
// WithDeadlineCause behaves like WithDeadline but also sets the cause of the
// returned Context when the deadline is exceeded. The returned CancelFunc does
// not set the cause.
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc)
// WithTimeoutCause behaves like WithTimeout but also sets the cause of the
// returned Context when the timout expires. The returned CancelFunc does
// not set the cause.
func WithTimeoutCause(parent Context, timeout time.Duration, cause error) (Context, CancelFunc)
與原先的 WithDeadline 和 WithTimeout 作用基本一致,唯一區(qū)別就是在形參上增加了 cause error,允許傳入錯誤類型。
WithTimeoutCause
WithTimeoutCause 的使用示例:
tooSlow := fmt.Errorf("too slow!")
ctx, cancel := context.WithTimeoutCause(context.Background(), 1*time.Second, tooSlow)
time.Sleep(2*time.Second)
cancel()
像上述程序,執(zhí)行 ctx.Err 方法時得到的結果是:context.DeadlineExceeded,這是既有的。
此時,我們再結合在 Go1.20 版本加入的 context.Cause 方法:
func Cause(c Context) error
就能得到對應的錯誤信息,上述的結果對應的是 tooSlow 變量。
WithCancelCause
WithCancelCause 的使用示例,計時器先觸發(fā):
finishedEarly := fmt.Errorf("finished early")
tooSlow := fmt.Errorf("too slow!")
ctx, cancel := context.WithCancelCause(context.Background())
ctx, _ = context.WithTimeoutCause(ctx, 1*time.Second, tooSlow)
time.Sleep(2*time.Second) // timer fires, setting the cause
cancel(finishedEarly) // no effect as ctx has already been canceled
對應的程序結果:
- ctx.Err():context.DeadlineExceeded 類型。
- context.Cause(ctx):tooSlow 類型。
先發(fā)生上下文取消的使用示例:
finishedEarly := fmt.Errorf("finished early")
tooSlow := fmt.Errorf("too slow!")
ctx, cancel := context.WithCancelCause(context.Background())
ctx, _ = context.WithTimeoutCause(ctx, 1*time.Second, tooSlow)
time.Sleep(500*time.Millisecond) // timer hasn't expired yet
cancel(finishedEarly) // cancels the timer and sets ctx.Err()
對應的程序結果:
- ctx.Err():context.Canceled 類型。
- context.Cause(ctx):finishedEarly 類型。
增加 AfterFunc
同樣的,在 Go1.21 也對 Context(上下文)被取消的動作后增加了一些增強。平時當上下文被取消時,我們只能通過啟動 Goroutine 來監(jiān)視取消行為并做一系列操作。
但這未免繁瑣且增大了我們的編碼和運行成本,因為每次處理都要 goroutine+select+channel 來一套組合拳,才能真正到寫自己業(yè)務代碼的地方。
為此新版本增加了注冊函數(shù)的功能,將會在上下文被取消時調(diào)用。函數(shù)簽名如下:
func AfterFunc(ctx Context, f func()) (stop func() bool)
在函數(shù)作用上,該函數(shù)會在 ctx 完成(取消或超時)后調(diào)用所傳入的函數(shù) f。
在運行機制上,它會自己在 goroutine 中調(diào)用 f。需要注意的是,即使 ctx 已經(jīng)完成,調(diào)用 AfterFunc 也不會等待 f 返回。
這也是可以套娃的,在 AfterFunc 里再套 AfterFunc。這里用不好也很容易 goroutine 泄露。
基于這個新函數(shù),可以看看以下兩個例子作為使用場景。
1、多 Context 合并取消的例子:
func WithFirstCancel(ctx1, ctx2 context.Context) (context.Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(ctx1)
stopf := context.AfterFunc(ctx2, func() {
cancel()
})
return ctx, func() {
cancel()
stopf()
}
}
2、在取消上下文時停止等待 sync.Cond:
func Wait(ctx context.Context, cond *sync.Cond) error {
stopf := context.AfterFunc(ctx, cond.Broadcast)
defer stopf()
cond.Wait()
return ctx.Err()
}
基本滿足了各種上下文的復雜訴求了。
總結
Context 一直是大家使用的最頻繁的標準庫之一,他聯(lián)通了整個 Go 里的工程體系。這次在 Go1.21 對 Context 增加了 WithXXXCause 相關函數(shù)的錯誤類型支持。對于我們在 Go 工程實踐中的排查和定位,能夠有一些不錯的助力。
另外 AfterFunc 函數(shù)的增加,看起來是個簡單的功能。但是可以解決以往的一些合并取消上下文和串聯(lián)處理的復雜場景,是一個不錯的擴展功能。
苛刻些,美中不足的就是,Go 都已經(jīng)發(fā)布 10+ 年了,加的還是有些太晚了。同時針對 Context 也需要有更體系的排查和定位側的補全了。