Go slog 入门:用结构化日志记录更清楚的服务行为

本文讲解 Go 标准库 log/slog 的基本用法,包括文本日志、JSON 日志、字段、With、错误记录和 HTTP 请求日志。

日志从字符串变成结构

传统日志经常这样写:

log.Printf("user %d login failed: %v", userID, err)

它能看,但机器不太好解析。后来很多服务会输出 JSON 日志,字段里有 leveltimeuser_idpathduration_ms。这样日志平台可以按字段搜索、聚合和告警。Go 标准库里的 log/slog 就是为了结构化日志提供统一接口。

学习 slog 的重点不是记住所有 API,而是理解日志应该记录什么:动作、关键字段、错误、耗时、请求范围信息。好的日志能帮助你在服务出问题时快速定位;糟糕的日志只是大量无意义字符串。

这篇文章从最小用法讲起,再写一个 HTTP 请求日志中间件。

最小 slog

logger := slog.Default()
logger.Info("server starting")

带字段:

logger.Info("server starting",
	"addr", ":8080",
	"env", "development",
)

错误日志:

if err != nil {
	logger.Error("load config failed", "err", err)
}

字段是成对出现的 key/value。key 通常使用短小稳定的英文,比如 user_idpathmethodduration_ms。不要把整段中文描述塞进 key,中文可以放在 message,但字段名最好稳定,方便查询。

文本日志和 JSON 日志

创建文本 handler:

logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
logger.Info("hello", "name", "Go")

创建 JSON handler:

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("hello", "name", "Go")

本地开发用文本日志比较舒服,生产环境常用 JSON 日志,方便日志平台解析。你可以通过配置决定:

func NewLogger(format string) *slog.Logger {
	switch format {
	case "json":
		return slog.New(slog.NewJSONHandler(os.Stdout, nil))
	default:
		return slog.New(slog.NewTextHandler(os.Stdout, nil))
	}
}

使用:

logger := NewLogger(os.Getenv("LOG_FORMAT"))

日志格式属于运行时配置,不应该写死在业务函数里。

With 添加公共字段

服务启动时可以创建带公共字段的 logger:

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)).
	With("service", "user-api").
	With("version", "v1.2.0")

之后每条日志都会带上这些字段。

请求级别也可以创建子 logger:

requestLogger := logger.With(
	"request_id", requestID,
	"method", r.Method,
	"path", r.URL.Path,
)

requestLogger.Info("request started")

这比每次手动写重复字段更清楚。With 不会修改原 logger,而是返回一个带额外字段的新 logger。

HTTP 请求日志中间件

先写状态码 recorder:

type statusRecorder struct {
	http.ResponseWriter
	status int
}

func (r *statusRecorder) WriteHeader(status int) {
	r.status = status
	r.ResponseWriter.WriteHeader(status)
}

中间件:

func RequestLogger(logger *slog.Logger, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		rec := &statusRecorder{
			ResponseWriter: w,
			status:         http.StatusOK,
		}

		next.ServeHTTP(rec, r)

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

使用:

mux := http.NewServeMux()
mux.HandleFunc("/healthz", healthHandler)

server := &http.Server{
	Addr:    ":8080",
	Handler: RequestLogger(logger, mux),
}

这条日志比普通字符串更容易搜索。你可以查询 status >= 500,也可以按 path 聚合耗时。

不要记录敏感信息

结构化日志更容易被系统收集、转发和长期保存,所以敏感信息更要谨慎。不要记录密码、token、完整身份证号、银行卡号、私密请求体。

错误示例:

logger.Info("login request",
	"email", req.Email,
	"password", req.Password,
)

正确做法:

logger.Info("login failed",
	"email", req.Email,
	"reason", "invalid_credentials",
)

即使是 email,也要看业务合规要求。日志不是数据库,不应该成为另一个敏感数据仓库。

小结

log/slog 让 Go 标准库拥有了结构化日志能力。你可以选择文本或 JSON handler,用 key/value 字段记录上下文,用 With 添加公共字段,用中间件记录 HTTP 请求。

日志的价值在于排查问题。每条关键日志都应该回答:发生了什么,和哪个用户或请求有关,在哪个路径,结果如何,耗时多久,错误是什么。字段稳定、信息克制、敏感数据不进日志,是结构化日志的基本要求。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页