Go 标准库 log 包需手动配置前缀、标志、文件输出及 panic 捕获才能满足生产需求,结构化日志应选用 zap 或 zerolog。
Go 标准库的 log 包本身不区分错误级别,直接用 log.Print 或 log.Printf 记录错误信息是可行的,但缺乏上下文、堆栈和结构化输出——这意味着你得自己补全这些能力,否则线上出问题时很难快速定位。
log.SetPrefix 和 log.SetFlags 标记错误来源标准 log 包虽无 Errorf 方法,但可通过前缀和标志增强可读性。常见错误是只调用 log.Println,结果日志里看不出是错误、时间戳缺失、也没有文件行号。
log.SetPrefix("ERROR: ") 统一标记错误日志,避免靠内容关键词 greplog.SetFlags(log.LstdFlags | log.Lshortfile) 启用时间戳 + 文件名+行号,定位快得多"[ERROR] " + err.Error(),前缀已承担该职责log.SetPrefix("ERROR: ")
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Printf("failed to open config: %v", err)
recover + debug.PrintStack
HTTP handler 或 goroutine 中未处理的 panic 会静默消失,仅靠 log.Printf 记录错误值根本不够。必须显式 recover 并打印完整堆栈,否则永远不知道 panic 发生在哪一行。
fmt.Sprintf("%+v", err) 对普通 error 没用,对 panic 无效;要用 debug.PrintStack()
defer func() {
if r := recover(); r != nil {
log.Printf("PANIC: %v", r)
debug.PrintStack()
}
}()
os.OpenFile 的 os.O_APPEND 和 os.O_CREATE
直接用 log.SetOutput(os.Stdout) 在开发时没问题,但生产环境必须写文件。常见错误是用 os.Create 每次覆盖日志,或者忘记设置权限导致进程无法写入。
os.O_APPEND | os.O_CREATE | os.O_WRONLY,缺一不可0644(非 0600),否则运维查日志要切用户os.OpenFile 返回的 error,空指针 panic 比日志丢失更难排查f, err := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatalf("failed to open log file: %v", err)
}
log.SetOutput(f)
结构化日志?别硬改标准库,换 zap 或 zerolog
当你要加 trace_id、字段过滤、JSON 输出、日志采样或对接 Loki/Splunk 时,标准 log 包的字符串拼接方式会迅速失控。强行给 log.Printf 包一层 map 转 JSON,性能差、易出错、且无法复用上下文。
zap.Logger.With(zap.String("path", r.URL.Path)) 可复用,log.Printf 每次都要重传zerolog.New(os.Stderr).With().Timestamp().Logger() 默认带时间,无需手动设 flagszap 的 Sugar 模式适合快速迁移,但结构化字段要用 Info() 配 String() 等方法logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
logger.Error().Err(err).Str("action", "save_user").Int64("user_id", id).Msg("failed")
真正难的不是“怎么记下错误”,而是确保错误发生时,那条日志里有足够线索让一个人在凌晨三点不用翻十页代码就能判断是网络超时、数据库约束冲突,还是上游返回了非法 JSON。前缀、堆栈、文件行号、结构化字段——每个都是省下五分钟的关键,而不是锦上添花的配置项。