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

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

大家好,我是煎鱼。

今天继续给大家分享 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 方法。

图片图片

函数签名如下:

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 业务工程的同学来讲,是个很不错的升级!终于打开了一个新的后门!

责任编辑:武晓燕 来源: 脑子进煎鱼了
相关推荐

2024-08-07 08:51:20

Go优化开发

2024-09-09 08:56:03

2024-08-01 08:47:04

Go计时器工程

2024-09-02 00:30:41

Go语言场景

2023-12-27 08:03:53

Go优化代码

2009-07-19 11:13:52

windows后门安全

2024-09-02 10:21:21

2023-10-09 07:14:42

panicGo语言

2021-09-05 18:25:30

Go命令仓库

2021-12-09 08:50:35

Kubernetes增强功能版本更新

2023-10-26 15:49:53

Go日志

2022-01-18 16:38:45

iOS苹果系统

2024-01-22 00:30:00

Go编程Go 1.22

2022-02-11 21:01:18

GoNetip网络库

2021-06-17 10:01:54

APT活动Victory 后门恶意软件

2015-07-03 12:38:51

2022-01-26 09:02:57

GoCut方法

2024-06-24 08:10:34

Java8表达式IDE

2021-09-01 07:21:46

堆栈Gopanic
点赞
收藏

51CTO技术栈公众号