Go服务崩溃监听

前言

假如程序会莫名down掉,我们必须记录日志方便后期排查原因。

这里就说下怎样记录崩溃日志。

程序

1
2
3
4
5
6
7
8
9
10
11
func main() {
defer func() {
if r := recover(); r != nil {
zlog.LogError(fmt.Sprintf("程序崩溃: %v\n堆栈: %s", r, debug.Stack()))
time.Sleep(100 * time.Millisecond) // 给日志一点时间写入
os.Exit(1)
}
}()

// 下面写原来的逻辑
}

代码说明

1
2
defer func() {
}()

这样写的作用是

登记一个匿名函数,注册到当前 goroutine 的退出队列不立即执行,在goroutine退出时执行。

defer:把后面的调用 注册到当前 goroutine 的退出队列不立即执行

逐行拆解:

代码 作用
defer func() { ... }() 把匿名函数注册成“main 函数返回前”一定会执行的逻辑;即使后面代码 panic 了,它也会被执行。
recover() 只在 defer 里有效;如果当前 goroutine 正在 panic,它会返回 panic 的值,并终止 panic 的传播;否则返回 nil
debug.Stack() 立即生成完整的函数调用栈(从 panic 点一路回溯到 main),方便定位是哪一行代码触发了 panic。
zlog.LogError(...) 把 panic 原因 + 堆栈写进你配置的日志文件(或控制台),而不是只打印到标准输出。
time.Sleep(100 * time.Millisecond) 给磁盘/文件句柄一点缓冲时间,确保日志真的落盘,防止进程立刻退出时日志还没写完。
os.Exit(1) 主动退出整个进程,返回码 1,告诉操作系统“我是异常退出的”,方便脚本、容器、守护进程识别。

协程中

我这里新建了一个协程执行逻辑

1
go handleMessages()

那么新建的协程中也要做相应的处理

1
2
3
4
5
6
7
8
func handleMessages() {
defer func() {
if r := recover(); r != nil {
zlog.LogError(fmt.Sprintf("handleMessages 崩溃: %v\n堆栈: %s", r, debug.Stack()))
}
}()
// 下面是原来的逻辑
}

log.Fatal

启动尽量不要用log.Fatal

1
log.Fatal(http.ListenAndServe(":"+config.PORT, nil))

我们可以在退出前做一些常见的操作(打印日志、发告警、尝试备用端口、让 systemd/k8s 重启策略接管等)再退出。

1
2
3
4
5
6
7
// ✅ 捕获 ListenAndServe 的错误,不要用 log.Fatal
err := http.ListenAndServe(":"+config.PORT, nil)
if err != nil {
zlog.LogError("HTTP 服务启动失败: " + err.Error())
time.Sleep(100 * time.Millisecond) // 给日志一点时间写入
os.Exit(1)
}