大家好,原因我是调函煎鱼。
在 Go 中有一个很经典的太久设计:context,这是速览设置数许多同学初学时必学的标准库。涉及到上下文传递、取消超时控制等必要项。原因
甚至在函数体中的调函第一个参数大多是传 context。写第三方库也必须兼容 context 设置,太久否则会经常有人提需求让你支持。
我觉得这次的新特性更新虽不复杂,但作用挺大。建议大家学习!
以下是一个快速 Demo:
package mainimport ( "context" "fmt" "time")const shortDuration = 1 * time.Millisecondfunc 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 的错误处理终于有了一点点的增强,来填补这个地方的信息,允许添加自定义的错误类型和信息。
新增的 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 的使用示例:
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 的使用示例,计时器先触发:
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 causecancel(finishedEarly) // no effect as ctx has already been canceled
对应的程序结果:
先发生上下文取消的使用示例:
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 yetcancel(finishedEarly) // cancels the timer and sets ctx.Err()
对应的程序结果:
同样的,在 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 也需要有更体系的排查和定位侧的补全了。
责任编辑:武晓燕 来源: 脑子进煎鱼了 GoContext设置(责任编辑:热点)
RTX4060跑分大量泄露:对比RTX3060 8GB提升近50%
《英雄联盟》衍生格斗游戏《Project L》新角色“亚索”演示
冠豪高新(600433.SH):重组事项获有条件通过 公司A股股票自3月12日起复牌
勒索病毒阻击战取得新突破,腾讯反病毒实验室成功解密被锁XP系统