Go1.23 新特性:再开后门,可以记录未捕获的 panic 和 throw 日志了!

大家好,我是煎鱼。

今天继续给大家分享 Go1.23 的新特性,这一轮里还是有不小有意思的惊喜的。其中不得不评本文中的这个新变化。必须得来一篇独立话题来提一下这个事!

过去学习写 Go 时,初学者入门的教程教一定会提到在使用 panic 时,强烈建议要使用 recover。否则在 goroutine 的场景下很容易出问题,也会导致记不来日志。

新版本后,终于有兜底 Go 程序崩溃的日志记录方法了!过于感人!

快速入门

panic+recover 例子

较为标准的 panic+recover 代码如下:

func mayPanic() {
 panic("脑子进煎鱼了!")
}

func main() {
 defer func() {
  if r := recover(); r != nil {
   fmt.Println("Recovered. Error:\n", r)
  }
 }()

 mayPanic()

 fmt.Println("煎鱼被烧着了")
}

输出结果:

Recovered. Error:
 脑子进煎鱼了!

常见的错误场景

想法很美好,有两个常见的错误的场景。很折磨人心态。

1、会有经常会有出现起了 goroutine,业务程序出现了预料之外的场景,导致出现了 panic,也没有 recover。此时如果外部没有统一的 recover,就会导致业务受阻。

2、更夸张的是 Go 内部源码偶尔会有触发使用 throw 函数,导致抛出致命错误的场景,最经典的是 map 并发读写导致的致命错误。

如下代码例子:

func main() {
 var wg sync.WaitGroup
 m := make(map[int]int)

 // 写操作
 wg.Add(1)
 go func() {
  defer wg.Done()
  for i := 0; i < 1000; i++ {
   m[i] = i
  }
 }()

 // 读操作
 wg.Add(1)
 go func() {
  defer wg.Done()
  for i := 0; i < 1000; i++ {
   _ = m[i]
  }
 }()

 wg.Wait()
 fmt.Println("煎鱼收工了!")
}

在运行程序结果时,会看到输出如下结果:

煎鱼收工了!

只要你多运行几次,有概率触发以下问题:

fatal error: concurrent map read and map write

goroutine 35 [running]:
main.main.func2()
 /Users/eddycjy/app/go/example/demo1/main.go:26 +0x6c
created by main.main in goroutine 1
 /Users/eddycjy/app/go/example/demo1/main.go:23 +0xe8

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0x140000021c0?)
 /opt/homebrew/Cellar/go/1.22.6/libexec/src/runtime/sema.go:62 +0x2c
...

这类致命错误是 recover 所无法捕获的。也因此在产线环境偶尔会出现这类纰漏导致的容器重启等问题。

问题背景

在 Go 编程中,现阶段很难很好地统一捕获 Go 程序的未知崩溃输出。崩溃会打印到 stderr,但是 Go 程序通常会将 stdout 和 stderr 用于其他目的。

虽然将其输出到 stderr 并没有错,但它会将两个输出混合在一起,使以后的分离更加困难。排查问题也需要查看所有大量的调试信息。

因此捕获未知的崩溃(无论是 panic 还是 thorew)对于事后调试和发送报告很有价值。

注:尤其是在 k8s 中很多是建议输出到 stdout、stderr 中的,这样在发生未知崩溃时,排查起来会更麻烦。

Go1.23 debug.SetCrashOutput

Go1.23 新版本中,本次在 runtime/debug 库中新增了 debug.SetCrashOutput 方法。

5159bb31b68f98358ae584cd2865484e.png

函数签名如下:

func SetCrashOutput(f *os.File, opts CrashOptions) error

代码例子:

import (
 "io"
 "log"
 "os"
 "os/exec"
 "runtime/debug"
)

func main() {
 monitor()

 println("煎鱼下午好!!!")
 // 没有被 recover 的未知错误
 panic("oops")
}

func monitor() {
 const monitorVar = "RUNTIME_DEBUG_MONITOR"
 if os.Getenv(monitorVar) != "" {
  // 实际演示 debug.SetCrashOutput 设置后的逻辑
  log.SetFlags(0)
  log.SetPrefix("monitor: ")

  crash, err := io.ReadAll(os.Stdin)
  if err != nil {
   log.Fatalf("failed to read from input pipe: %v", err)
  }
  if len(crash) == 0 {
   os.Exit(0)
  }

  f, err := os.CreateTemp("", "*.crash")
  if err != nil {
   log.Fatal(err)
  }
  if _, err := f.Write(crash); err != nil {
   log.Fatal(err)
  }
  if err := f.Close(); err != nil {
   log.Fatal(err)
  }
  log.Fatalf("saved crash report at %s", f.Name())
 }

 // 模拟应用程序进程,设置 debug.SetCrashOutput 值
 exe, err := os.Executable()
 if err != nil {
  log.Fatal(err)
 }
 cmd := exec.Command(exe, "-test.run=ExampleSetCrashOutput_monitor")
 cmd.Env = append(os.Environ(), monitorVar+"=1")
 cmd.Stderr = os.Stderr
 cmd.Stdout = os.Stderr
 pipe, err := cmd.StdinPipe()
 if err != nil {
  log.Fatalf("StdinPipe: %v", err)
 }
 debug.SetCrashOutput(pipe.(*os.File), debug.CrashOptions{})
 if err := cmd.Start(); err != nil {
  log.Fatalf("can't start monitor: %v", err)
 }

}

输出结果:

$ go run main.go
煎鱼下午好!!!
panic: oops

goroutine 1 [running]:
main.main()
 /Users/eddycjy/app/go/example/demo1/main.go:15 +0x48
exit status 2
monitor: saved crash report at /var/folders/y8/whksnvd17qn8bgs17yh_y59m0000gn/T/92172971.crash

崩溃后的文件记录:

$ cat /var/folders/y8/whksnvd17qn8bgs17yh_y59m0000gn/T/92172971.crash
panic: oops

goroutine 1 [running]:
main.main()
 /Users/eddycjy/app/go/example/demo1/main.go:15 +0x48

非常顺利的记录到未 recover 的 panic 导致的 crash 了。

总结

本次 Go1.23 在 runtime/debug 中新增了 debug.SetCrashOutput 方法来允许设置未被捕获的错误、异常的日志写入。可用于为所有 Go 进程意外崩溃构建自动报告机制!

这个变动虽然不大,但是对于我们日常写 Go 业务工程的同学来讲,是个很不错的升级!终于打开了一个新的后门!

推荐阅读

关注和加煎鱼微信,

一手消息和知识,拉你进技术交流群👇

0719c244c7fa28e6a83a7b6e472a6723.jpeg

7213de713e831f65ffe1a5d27e59c3e5.png

你好,我是煎鱼,出版过 Go 畅销书《Go 语言编程之旅》,再到获得 GOP(Go 领域最有观点专家)荣誉,点击蓝字查看我的出书之路

日常分享高质量文章,输出 Go 面试、工作经验、架构设计,加微信拉读者交流群,和大家交流!

原创不易 点赞支持

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值