如何在Golang中记录错误日志_Golang错误日志记录方式

log.Printf 不显示调用栈因不自动捕获堆栈,需用 runtime.Caller 或 zap.Error();zap.Error() 支持错误展开与 nil 安全,优于直接 err.Error();log.Fatal 会终止进程,HTTP handler 中禁用;敏感字段必须脱敏,日志重在可追溯而非全量输出。

log.Printf 记录错误时,为什么看不到调用栈?

因为 log.Printf 只做格式化输出,不自动捕获堆栈。你传入的 err 本身(比如 errors.New("xxx"))通常不含调用信息。要看到哪一行出的错,得手动加 runtime.Caller,或者换支持 trace 的日志库。

临时调试可以这样补:

import "runtime"

func logError(err error) {
    _, file, line, _ := runtime.Caller(1)
    log.Printf("ERROR [%s:%d] %v", file, line, err)
}
  • 别在生产环境高频调用 runtime.Caller,有性能开销
  • Caller(1) 表示跳过当前函数,取调用方的位置
  • 标准库 log 没有内置 error 字段结构化能力,err.Error() 是唯一能安全取的字符串

zap 记录错误时,zap.Error() 和直接 zap.String("error", err.Error()) 有什么区别?

zap.Error() 不只是把 err.Error() 存成字符串——它会尝试展开实现了 fmt.FormatterstackTracer 接口的错误(比如 github.com/pkg/errors 包 wrap 的错误),并保留原始类型信息,方便后期结构化过滤或高亮显示。

推荐写法:

logger.Error("db query failed",
    zap.String("query", sql),
    zap.Error(err), // ← 这里传 err 本体,不是 err.Error()
    zap.String("user_id", userID),
)
  • 如果 errnilzap.Error() 会自动写成 "error": null,不会 panic
  • 自己用 zap.String("error", err.Error()) 会丢失堆栈、丢掉类型语义,且 errnil 时会 panic
  • 注意:原生 errors.Newfmt.Errorf(Go 1.13+)不带堆栈,需用 fmt.Errorf("xxx: %w", err) 配合 errors.Is/As 才能链式追踪

为什么 log.Fatal 不适合记录 HTTP handler 中的错误?

log.Fatal 底层调用 os.Exit(1),会导致整个进程退出。在 HTTP server 里一旦某个请求出错就 kill 掉服务,显然不可接受。

  • handler 中该用 log.Printf 或结构化 logger 记录,然后返回 http.Error 或自定义响应
  • log.Panic 同样危险:触发 panic 后若没被 recover,照样崩进程
  • 真正该用 log.Fatal 的场景极少,一般是 main 函数里初始化失败(如无法监听端口、加载配置失败)

错误日志中要不要打印敏感字段(如密码、token、用户手机号)?

不要。哪怕是在 debug 环境,也不该让明文敏感数据落到磁盘日志文件里。zap 提供 zap.String("password", "[redacted]") 这种显式脱敏,但更稳妥的是从源头控制:

  • 记录前用 strings.ReplaceAll 或正则擦除已知敏感键值(如 "auth_token""card_number"
  • HTTP 请求体、数据库查询参数等,统一走中间件或 wrapper 做字段过滤,而不是靠日志函数临时判断
  • 使用 zap.Object + 自定义 LogObjectMarshaler 接口,对 struct 字段做选择性序列化

日志不是 dump,是线索。留够定位信息就行,多一条密码反而增加泄露风险。