Go 结构化日志实践指南
wxk1991 Lv5

Go 结构化日志实践指南

日志不是把字符串打印出来就完事。真实服务里,日志要能搜索、聚合、告警、追踪请求链路。结构化日志的价值,就是让日志从“人眼阅读”变成“系统可分析”。

Go 1.21 之后标准库提供了 log/slog,很多项目已经不需要额外引入日志库。


一、使用 slog 输出 JSON

1
2
3
4
5
6
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

logger.Info("server started",
"addr", ":8080",
"env", "prod",
)

输出会是 JSON 格式,适合被日志平台采集。


二、错误日志要带上下文

不要只写:

1
logger.Error("request failed")

更好的写法:

1
2
3
4
5
6
logger.Error("request failed",
"method", r.Method,
"path", r.URL.Path,
"user_id", userID,
"error", err,
)

线上排查时,日志缺上下文就等于没写。


三、请求级字段可以提前绑定

同一个请求里的日志通常都需要 trace id、用户 id、路径等字段:

1
2
3
4
5
6
7
reqLogger := logger.With(
"trace_id", traceID,
"user_id", userID,
)

reqLogger.Info("query user")
reqLogger.Info("call payment service")

这样每条日志都会自动带上这些字段。


四、日志级别要有边界

常见级别可以这样理解:

  • Debug:本地调试或临时排查。
  • Info:关键生命周期事件。
  • Warn:有异常但还能继续。
  • Error:当前操作失败,需要关注。

不要把所有东西都打成 Error。告警系统看到大量假错误,很快就会失去意义。


五、不要记录敏感信息

日志里不要出现密码、token、身份证、银行卡、完整手机号等敏感数据。

如果必须记录用户标识,优先使用内部用户 ID。对邮箱、手机号这类信息,要脱敏。


六、在 HTTP 中间件里记录请求日志

1
2
3
4
5
6
7
8
9
10
11
12
13
func accessLog(logger *slog.Logger, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()

next.ServeHTTP(w, r)

logger.Info("http request",
"method", r.Method,
"path", r.URL.Path,
"cost_ms", time.Since(start).Milliseconds(),
)
})
}

真实项目还应该记录状态码、请求大小、响应大小和 trace id。


七、实践建议

Go 项目如果没有特殊需求,可以优先使用 log/slog。它足够简单,也能满足大多数结构化日志场景。

日志的关键不是多,而是可定位。每条关键日志都应该回答三个问题:发生了什么,影响了谁,能不能顺着字段继续查下去。