大家好,我是煎鱼。
在 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)
与原先的 WithDeadline 和 WithTimeout 作用基本一致,唯一区别就是在形参上增加了 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 也需要有更体系的排查和定位侧的补全了。