大家好,我是煎鱼。

在 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/blo… 已收录,学习 Go 言语能够看 Go 学习地图和路线,欢迎 Star 催更。

Go 图书系列

  • Go 言语入门系列:初探 Go 项目实战
  • Go 言语编程之旅:深化用 Go 做项目
  • Go 言语规划哲学:了解 Go 的为什么和规划思考
  • Go 言语进阶之旅:进一步深化 Go 源码

引荐阅览

  • Go1.21 速览:新内置函数 clear、min、max 和新规范库包 cmp!
  • Go1.21 速览:过了一年半,slices、maps 泛型库总算要参加规范库。。。
  • Go1.21 速览:Go 总算计划进一步支撑 WebAssembly 了。。。