1

大家好,我是煎鱼。

在 Go 中有一个很经典的设计:context,这是许多同学初学时必学的标准库。涉及到上下文传递、超时控制等必要项。

甚至在函数体中的第一个参数大多是传 context。写第三方库也必须兼容 context 设置,否则会经常有人提需求让你支持。

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

一切都看起来没什么问题。

麻烦点

但在实际写业务代码和排查问题时,你就会发现一个麻烦的事。在出现上下文超时或到达所设置的截止时间时,ctx.Err 方法可以获得 context deadline exceeded 的错误信息。

但这是远远不够的,你只知道是因为诱发了超时。但不知道是哪里导致的,还得再去根据访问的逻辑,再走一遍脑洞,再进行排查。又或是根据代码堆栈,再去设想,最后复现成功。

又或是查不到。因为这种一般是偶现,很有可能就留给下一代的继承者了~

又更有业务诉求,希望在出现上下文的异常场景时,可以及时执行回调方法。然而这没有太便捷的实现方式。

Go1.21 增强 Context

增加 WithXXXCause

在即将发布的 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)

与原先的 WithDeadlineWithTimeout 作用基本一致,唯一区别就是在形参上增加了 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() 

像上述程序,执行 ctx.Err 方法时得到的结果是:context.DeadlineExceeded,这是既有的。

此时,我们再结合在 Go1.20 版本加入的 context.Cause 方法:

func Cause(c Context) error

就能得到对应的错误信息,上述的结果对应的是 tooSlow 变量。

WithCancelCause

WithCancelCause 的使用示例,计时器先触发:

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 类型。

先发生上下文取消的使用示例:

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 来监视取消行为并做一系列操作。

但这未免繁琐且增大了我们的编码和运行成本,因为每次处理都要 goroutine+select+channel 来一套组合拳,才能真正到写自己业务代码的地方。

为此新版本增加了注册函数的功能,将会在上下文被取消时调用。函数签名如下:

func AfterFunc(ctx Context, f func()) (stop func() bool)

在函数作用上,该函数会在 ctx 完成(取消或超时)后调用所传入的函数 f。

在运行机制上,它会自己在 goroutine 中调用 f。需要注意的是,即使 ctx 已经完成,调用 AfterFunc 也不会等待 f 返回。

这也是可以套娃的,在 AfterFunc 里再套 AfterFunc。这里用不好也很容易 goroutine 泄露。

基于这个新函数,可以看看以下两个例子作为使用场景。

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 一直是大家使用的最频繁的标准库之一,他联通了整个 Go 里的工程体系。这次在 Go1.21 对 Context 增加了 WithXXXCause 相关函数的错误类型支持。对于我们在 Go 工程实践中的排查和定位,能够有一些不错的助力。

另外 AfterFunc 函数的增加,看起来是个简单的功能。但是可以解决以往的一些合并取消上下文和串联处理的复杂场景,是一个不错的扩展功能。

苛刻些,美中不足的就是,Go 都已经发布 10+ 年了,加的还是有些太晚了。同时针对 Context 也需要有更体系的排查和定位侧的补全了。

文章持续更新,可以微信搜【脑子进煎鱼了】阅读,本文 GitHub github.com/eddycjy/blog 已收录,学习 Go 语言可以看 Go 学习地图和路线,欢迎 Star 催更。

Go 图书系列

推荐阅读


煎鱼
8.4k 声望12.8k 粉丝