“日志不是写给机器看的,是写给人类在半夜三点 debug 时救命用的。”
Go 1.21 终于带来了官方结构化日志包 —— log/slog。从此,我们不再需要在 zap、zerolog、logrus 之间反复横跳,也不用担心团队里有人偷偷用 fmt.Println 写日志了(好吧,可能还是会有)。
本文将带你从零上手 slog,并通过几个小例子展示它如何让日志变得更结构化、可查询、可维护——甚至还能帮你保住头发。
🧱 一、slog 的三大核心:Logger、Handler、Record
slog 的设计哲学很清晰:前端 API + 后端实现分离。
- Logger:你每天打交道的“前台”,调用 Info()、Error() 的地方。
- Handler:幕后英雄,决定日志长什么样、写到哪(比如 JSON 还是文本?stdout 还是文件?)。
- Record:中间传话筒,封装了时间、级别、消息、属性等原始数据。
想象一下:Logger 是你点外卖的 App,Handler 是后厨,Record 就是你点的“宫保鸡丁 + 微辣 + 不要花生”。
最简示例
package main
import (
"log/slog"
"os"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("用户登录成功", "user_id", 123)
}
输出:
{"time":"2026-01-28T10:00:00Z","level":"INFO","msg":"用户登录成功","user_id":123}
✅ 结构化!✅ 可被日志系统解析!✅ 没有乱七八糟的 fmt.Printf!
⚠️ 二、一个致命陷阱:别再用 key, value 了!
很多人图省事这么写:
logger.Warn("权限不足", "user_id", 123, "resource") // ❌ 少了个值!
结果?slog 不会报错,而是默默生成一个 !BADKEY 字段:
{"!BADKEY":"resource", …}
😱 想象一下:线上故障,你查日志发现关键字段变成了 !BADKEY,而老板正盯着你……
✅ 正确姿势:用 slog.Attr
logger.Warn("权限不足",
slog.Int("user_id", 123),
slog.String("resource", "/api/admin"),
)
这样,编译器就能帮你抓 bug,而不是等到凌晨三点才暴露问题。
💡 小贴士:配合 golangci-lint + sloglint 插件,强制全项目使用 slog.Attr,彻底杜绝 !BADKEY!
# .golangci.yml
linters:
enable:
– sloglint
settings:
sloglint:
attr-only: true
🛠️ 三、实战:HTTP 请求自动带上 trace ID
在微服务中,请求链路追踪离不开 correlation_id 或 trace_id。slog 配合社区库 slog-context 能轻松实现。
场景:每个 HTTP 请求自动生成唯一 ID,并自动注入日志
package main
import (
"log/slog"
"net/http"
"os"
"github.com/google/uuid"
slogctx "github.com/veqryn/slog-context"
)
func requestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cid := r.Header.Get("X-Correlation-ID")
if cid == "" {
cid = uuid.New().String()
}
ctx := slogctx.Prepend(r.Context(), slog.String("cid", cid))
r = r.WithContext(ctx)
w.Header().Set("X-Correlation-ID", cid)
next.ServeHTTP(w, r)
})
}
func hello(w http.ResponseWriter, r *http.Request) {
slog.InfoContext(r.Context(), "处理请求")
w.Write([]byte("Hello, slog!"))
}
func main() {
handler := slogctx.NewHandler(slog.NewJSONHandler(os.Stdout, nil), nil)
slog.SetDefault(slog.New(handler))
mux := http.NewServeMux()
mux.HandleFunc("/", hello)
http.ListenAndServe(":8080", requestID(mux))
}
访问 curl http://localhost:8080,你会看到:
{
"time": "…",
"level": "INFO",
"msg": "处理请求",
"cid": "a1b2c3d4-…",
…
}
✅ 所有日志自动带上 cid! ✅ 无需手动传递! ✅ 故障排查时一键过滤整个请求链路!
🔐 四、敏感信息防护:用 LogValuer 控制输出
直接打日志可能泄露密码、身份证等敏感信息:
type User struct {
ID string
Email string
Password string // ❌ 千万别打出来!
}
✅ 解决方案:实现 LogValuer 接口
func (u *User) LogValue() slog.Value {
return slog.GroupValue(
slog.String("id", u.ID),
slog.String("email", u.Email),
// 故意不输出 Password
)
}
现在:
logger.Info("创建用户", slog.Any("user", &user))
输出只有安全字段:
{
"msg": "创建用户",
"user": {
"id": "u123",
"email": "alice@example.com"
}
}
🛡️ 安全是底线,日志也不例外!
📊 五、性能怎么样?能用在生产吗?
slog 的性能确实不如 zerolog 或 zap(大约慢 5~6 倍),但对绝大多数应用完全够用。
| zerolog | 380 | 1 |
| slog | 2480 | 42 |
如果你的服务每秒处理 10 万请求,且每条都打日志——那你可能需要 zap。 但如果你只是普通 Web 服务?放心用 slog,省下的心智负担远超那几微秒。
🚀 六、进阶建议
-
日志级别动态调整:用 slog.LevelVar 实现不停机调 DEBUG。
-
多输出:用 slog-multi 同时写 stdout + 文件 + 网络。
-
避免重复 key:用 slog-dedup 处理冲突。
✅ 总结
| 官方支持 | 无需引入第三方依赖(基础场景) |
| 结构化 | JSON 输出天然适配现代日志平台 |
| 可扩展 | Handler 可插拔,轻松集成 |
| 安全可控 | LogValuer 防止敏感信息泄露 |
| 上下文友好 | 与 context 深度集成 |
结论:除非你有极端性能要求,否则 slog 是 Go 应用日志的首选方案。
网硕互联帮助中心






评论前必须登录!
注册