Go 服务一旦在线上出现 panic、接口报错或行为异常,日志往往就是还原现场的第一手材料。但真正决定排障效率的,不是“有没有日志”,而是日志里是否留下了时间、调用位置、异常内容和上下文字段。下面就按常见工具拆开来看:标准库 log 适合快速起步,logrus 更强调可读性和字段扩展,zap 则更适合高并发下的结构化记录。读完你可以更清楚地判断,小项目该怎么配,线上服务又该把哪些信息优先打出来。
为什么异常定位先看日志
在 Go 项目里,很多问题并不会直接把根因摆在你面前。线上服务突然崩溃、某个请求偶发失败、业务逻辑在特定输入下触发 panic,这些情况如果没有足够完整的日志,排查通常只能靠猜。
所以异常日志至少要回答几个问题:问题发生在什么时候、发生在哪个文件或函数、触发时拿到了什么上下文、程序最终抛出了什么错误或 panic。只要这几类信息缺失,后续再接入日志平台,检索效果也会大打折扣。
从实践上看,Go 里做异常定位并不复杂,重点通常只有三件事:把日志格式配完整、在关键位置捕获 panic、把异常内容写成便于检索的字段。
用标准库 log 先把基础信息打全
如果项目体量不大,或者你只是先把排障链路补起来,标准库 log 已经够用。它不需要额外依赖,适合小型服务、脚本工具和原型项目。
原文示例里,核心做法是两步:先设置输出目标和日志标志,再用 defer + recover 捕获 panic。
package main
import (
"log"
"os"
)
func main() {
log.SetOutput(os.Stdout)
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Println("This is an info message")
log.Printf("This is a formatted %s message", "info")
defer func() {
if r := recover(); r != nil {
log.Printf("Recovered from panic: %v", r)
}
}()
panic("An error occurred")
}
这段代码里值得注意的是 log.SetFlags(log.LstdFlags | log.Lshortfile)。其中 log.LstdFlags 会带上标准时间信息,log.Lshortfile 会输出短文件名和行号。对于排查异常来说,这两个信息已经能帮你先完成一次有效缩圈:先按时间对齐请求,再按文件位置追代码路径。
而 defer + recover 的作用,是在程序发生 panic 时避免现场信息直接丢失。即便服务已经进入异常流程,你仍然能从日志里看到 Recovered from panic: %v 对应的具体内容,从而知道触发的不是普通业务错误,而是运行时级别的异常。
它的局限也很明显:标准库更偏向纯文本输出,字段化能力有限。如果你后面需要按用户 ID、请求 ID、模块名、错误类型做精确筛选,就会开始感到吃力。
logrus 适合补齐字段和调用信息
当项目进入多人协作、模块增多、日志需要统一管理的阶段,logrus 往往比标准库更顺手。它的优势不只是格式更灵活,更重要的是可以把上下文信息作为字段绑定到日志里。
package main
import (
"github.com/sirupsen/logrus"
)
func main() {
logrus.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
CallerPrettyfier: func(f *runtime.Frame) (string, string) {
filename := f.File
if filepath.Base(filename) == "logrus.go" {
filename = filepath.Base(filepath.Dir(filename))
}
return filepath.Base(filename), f.Function
},
})
logrus.Info("This is an info message")
logrus.WithFields(logrus.Fields{
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
defer func() {
if r := recover(); r != nil {
logrus.WithFields(logrus.Fields{
"error": r,
}).Error("Recovered from panic")
}
}()
panic("An error occurred")
}
这一版最大的变化,是把异常相关信息从“拼在一句话里”变成了“写进独立字段里”。例如 WithFields(logrus.Fields{ ... }) 可以同时记录多个上下文值,后面无论是人工阅读还是接入分析工具,都比纯文本检索更直观。
CallerPrettyfier 也很关键。它允许你自定义调用位置的显示方式,把文件名和函数名整理成更适合阅读的格式。对于链路较深的服务,这一点很有用,因为异常不一定出在报错函数本身,调用栈附近的文件和函数名常常是更快的入口。
同样地,panic 捕获仍然是通过 defer 和 recover 完成,只不过这里把 error 放进了字段中。这样后续接到日志平台后,可以直接筛选某个字段值,而不是靠全文搜索一段错误描述。
高并发场景为什么常用 zap
如果服务对吞吐和延迟更敏感,zap 往往是更常见的选择。它的重点不只是“能打日志”,而是把结构化和性能一起考虑进去,尤其适合请求量大、日志量高的服务。

package main
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func main() {
config := zap.NewProductionConfig()
config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
logger, _ := config.Build()
defer logger.Sync()
logger.Info("This is an info message",
zap.String("animal", "walrus"),
zap.Int("size", 10),
)
defer func() {
if r := recover(); r != nil {
logger.Error("Recovered from panic",
zap.Error(r),
)
}
}()
panic("An error occurred")
}
示例里先通过 zap.NewProductionConfig() 创建生产环境配置,再把时间编码方式设置为 zapcore.ISO8601TimeEncoder。这样输出的时间字段更标准,便于跨系统对齐时间线。
和 logrus 类似,zap 也是结构化日志,但它把字段定义得更明确,例如 zap.String、zap.Int。这种写法的直接好处是日志平台可以更稳定地按键值对建立索引,查询时不需要反复处理自由格式文本。
不过这段示例有一个值得留意的细节:recover() 返回的是 interface{},而代码中写的是 zap.Error(r)。从 Go 的类型约束看,zap.Error 期望的是 error 类型,实际项目里通常需要先确认 panic 值是不是 error,否则更稳妥的做法是按普通字段或字符串记录。原文想表达的重点没有问题,即把 panic 内容结构化保存下来,便于后续在 Elasticsearch、Loki 这类系统里按字段过滤。
真正排查异常时,重点看这三步
无论你最后选的是标准库还是第三方日志库,定位异常的基本路径其实是一致的。

1. 先确认日志最小必备字段
至少要有时间戳、文件名或调用位置。如果业务复杂,最好进一步带上函数名、请求标识以及关键上下文字段。没有这些信息,日志再多也只是噪声。
2. 在可能 panic 的路径保留现场
通过 defer + recover 兜住异常,把 panic 内容输出到日志。这样你至少能知道程序是在哪里中断、抛出了什么,而不是只看到服务挂掉后的结果。
3. 按时间线和字段回溯根因
真正分析日志时,不要只盯着报错那一行。更有效的方式是先按时间定位到异常窗口,再结合字段筛出同一请求、同一模块或同一对象的上下文日志,最后回到对应代码位置确认触发条件。
换句话说,日志工具只是载体,异常定位的关键还是让日志变成可检索、可对比、可追溯的数据。标准库 log 适合先把基础能力补齐,logrus 更适合需要丰富字段的常规项目,zap 则更偏向高性能生产场景。工具不同,目标是一致的:让异常排查从凭经验猜测,变成沿着日志证据逐步还原现场。







