slog 是 Go 1.21 加入标准库的结构化日志包,为 Go 提供了统一的日志 API、日志级别、结构化字段和 Handler 扩展机制。
PS: golang.org/x/exp/slog 转正,变成了 log/slog
对于新项目,推荐直接使用 log/slog;对于已有 zap、logrus 等日志系统,也可以通过逐步迁移的方式引入,不需要一次性重构。
一、核心模型:Logger + Handler + Record
slog 的核心由三个部分组成:
- Logger:业务代码使用的日志 API。
- Handler:决定日志如何处理和输出。
- Record:一条完整的日志记录。
最常见的使用方式:
logger := slog.New(
slog.NewJSONHandler(os.Stdout, nil),
)
logger.Info("user login",
"user_id", 123,
)
一条日志可以理解为:
time + level + message + attributes
例如:
{
"time": "2026-08-26T01:00:00Z",
"level": "INFO",
"msg": "user login",
"user_id": 123
}
slog 提供了包级函数:
slog.Info(...)
slog.Warn(...)
slog.Error(...)
这些函数实际上使用的是 slog.Default()。
如果需要自定义 Logger,则使用:
logger := slog.New(handler)
工程项目中建议将 Logger 作为依赖传递,而不是让业务代码大量依赖全局 slog。
二、日志级别
slog 内置四个标准级别:
Debug (-4)
Info ( 0)
Warn ( 4)
Error ( 8)
默认级别是 Info,因此 Debug 日志默认不会输出。
可以通过 HandlerOptions 设置:
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
AddSource: true,
})
生产环境通常建议:
Debug → 默认关闭
Info → 正常业务日志
Warn → 异常但可以继续运行
Error → 操作失败或需要关注的问题
不要滥用 Error。例如用户输入错误、资源不存在等通常属于业务层面的 Info/Warn,而不是系统错误。
三、生产环境推荐配置
Web 服务、微服务等后端程序,推荐:
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
AddSource: true,
})
logger := slog.New(handler)
slog.SetDefault(logger)
生产环境推荐:
应用
↓
JSON
↓
stdout/stderr
↓
Docker / Kubernetes
↓
日志采集系统
↓
Loki / Elasticsearch / ClickHouse / 云日志平台
不要让业务程序自己承担完整的日志基础设施职责。
特别是在 Kubernetes 环境中,应用通常只需要输出到 stdout/stderr,日志采集、存储、轮转和生命周期管理交给平台。
写入文件
package main
import (
"log/slog"
"os"
)
func main() {
f, err := os.Create("app.log")
if err != nil {
panic(err)
}
defer f.Close()
logger := slog.New(slog.NewJSONHandler(f, nil))
slog.SetDefault(logger)
slog.Info("greeting", "say", "hello")
}
四、不要过度使用 ReplaceAttr
slog.HandlerOptions.ReplaceAttr 可以修改日志字段,例如:
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// 修改字段
return a
},
})
它适合做统一的日志 Schema 转换,例如:
- 修改字段名称
- 删除敏感字段
- 修改 source 格式
- 对 level 做统一映射
- 对特定字段做脱敏
但是不要为了“格式好看”而大量修改标准字段。
尤其是时间字段,JSONHandler 已经能够提供标准化的时间表示。如果没有兼容现有日志平台的实际需求,没有必要重新格式化。
五、结构化日志的三个核心 API
1. With:注入公共字段
对于整个服务、模块或者请求生命周期都不会变化的字段,可以使用 With:
logger := slog.Default().With(
"service", "order",
"version", "1.0.0",
)
之后:
logger.Info("create order")
自动包含:
{
"service": "order",
"version": "1.0.0",
"msg": "create order"
}
适合:
service
version
env
host
module
2. Group:组织复杂字段
例如 HTTP 请求:
logger.Info("request",
slog.Group("http",
slog.String("method", "GET"),
slog.String("path", "/users"),
slog.Int("status", 200),
),
)
JSON 可以形成嵌套结构:
{
"msg": "request",
"http": {
"method": "GET",
"path": "/users",
"status": 200
}
}
对于复杂日志 Schema,Group 比简单堆积大量字段更加清晰。
WithGroup
WithGroup 返回带命名空间的子 Logger,后续字段自动归入该分组,适合组件级归类(db/http/kafka)。slog.Group 仅单次日志内联嵌套,而 WithGroup 是持久命名空间,一次定义、全局生效,日志查询时按分组过滤更高效。
// 创建带 db 分组的子 Logger
dbLogger := slog.WithGroup("db")
// 后续所有日志自动归入 db 命名空间
dbLogger.Info("query finished",
"table", "orders",
"rows", 12,
"cost_ms", 45,
)
// WithGroup 输出 — 字段归入 db 对象
// {"time":"...","level":"INFO","msg":"query finished","db":{"table":"orders","rows":12,"cost_ms":45}}
// 对比:平铺 With 方式
flatLogger := slog.With("table", "orders")
flatLogger.Info("query finished", "rows", 12)
// With 输出 — 字段平铺
// {"time":"...","level":"INFO","msg":"query finished","table":"orders","rows":12}
3. LogAttrs:高频日志使用
slog 同时支持:
logger.Info("request", "status", 200)
和:
logger.LogAttrs(
ctx,
slog.LevelInfo,
"request",
slog.Int("status", 200),
)
LogAttrs 直接接收 slog.Attr,可以减少 key-value 参数转换带来的部分开销。
因此在:
- 高频日志
- 热循环
- 高吞吐网关
- 性能敏感服务
中,可以优先考虑 LogAttrs。
但是不要把“LogAttrs 更快”理解成绝对结论。实际性能取决于 Handler、字段类型、日志级别以及是否真的产生日志,是否有性能收益应该通过 benchmark 验证。
业务普通日志优先考虑可读性,不要为了微小性能收益到处使用 LogAttrs。
六、Context 不会自动产生 TraceID
这是使用 slog 时非常容易误解的一点。
slog.InfoContext(ctx, "request")
并不会自动把:
ctx.Value("trace_id")
写入日志。
Context 在这里主要用于向 Handler 传递上下文信息。
如果希望自动增加 TraceID,可以实现一个包装 Handler:
type ContextHandler struct {
slog.Handler
}
func (h *ContextHandler) Handle(
ctx context.Context,
r slog.Record,
) error {
if traceID, ok := ctx.Value("trace_id").(string); ok {
r.AddAttrs(
slog.String("trace_id", traceID),
)
}
return h.Handler.Handle(ctx, r)
}
请求链路可以设计为:
HTTP Request
↓
提取/生成 TraceID
↓
context.WithValue
↓
业务函数
↓
slog.InfoContext(ctx, ...)
↓
ContextHandler
↓
JSONHandler
这样日志平台可以通过:
trace_id = xxx
查询整个请求链路。
七、Context 不应该成为“万能参数袋”
虽然可以把 TraceID、RequestID 等信息放入 Context,但不要把业务对象、配置、数据库连接等大量数据全部塞进去。
建议 Context 主要承担:
- 请求生命周期
- deadline / cancellation
- tracing
- 少量 request-scoped metadata
而业务依赖应该明确通过参数或者依赖注入传递。
例如:
type UserService struct {
logger *slog.Logger
}
func NewUserService(logger *slog.Logger) *UserService {
return &UserService{
logger: logger,
}
}
这样比在每个函数里调用:
slog.Default()
更加容易测试和维护。
八、业务代码不要到处使用全局 slog
不推荐:
func CreateOrder() {
slog.Info("create order")
}
更推荐:
type OrderService struct {
logger *slog.Logger
}
func (s *OrderService) CreateOrder(ctx context.Context) {
s.logger.InfoContext(ctx, "create order")
}
初始化时:
logger := slog.Default().With(
"service", "order",
)
service := NewOrderService(logger)
这样做的收益包括:
- 依赖显式
- 方便单元测试
- 方便替换 Handler
- 可以注入 service/module 字段
- 避免业务代码依赖全局状态
包级 slog.Info() 等 API 可以主要用于 main、启动过程以及非常简单的基础设施代码。
九、错误日志
推荐统一使用 err 字段:
logger.ErrorContext(
ctx,
"create order failed",
"order_id", orderID,
"err", err,
)
slog 可以处理 error 类型,因此通常没有必要主动:
err.Error()
如果日志平台要求 err 必须是字符串,再使用:
slog.String("err", err.Error())
关键不是“error 一定应该使用哪一种”,而是整个项目保持统一的日志 Schema。
例如统一:
err
error_code
operation
而不要一个模块使用:
error
另一个使用:
exception
第三个又使用:
message
十、LevelVar:动态调整日志级别
如果希望生产环境动态打开 Debug,可以使用 slog.LevelVar:
var level slog.LevelVar
level.Set(slog.LevelInfo)
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: &level,
})
之后可以动态:
level.Set(slog.LevelDebug)
因此可以将它和:
- 配置中心
- Admin API
- 运维控制台
- 动态配置系统
结合,实现:
线上问题
↓
临时开启 Debug
↓
复现问题
↓
关闭 Debug
无需重新编译和发布服务。
生产环境应避免长期保持 Debug,否则容易产生大量无效日志和额外成本。
十一、日志轮转交给基础设施
Kubernetes
推荐:
Application
↓
stdout
↓
Container Runtime
↓
Log Collector
↓
Log Storage
应用不需要自己实现日志文件轮转。
裸机 / VM
如果应用确实需要直接写文件,可以使用:
- Linux 日志轮转工具
logrotate - Golang 日志轮转库
lumberjack - 系统日志服务
例如 lumberjack:
r := &lumberjack.Logger{
Filename: "./foo.log",
LocalTime: true,
MaxSize: 1,
MaxAge: 3,
MaxBackups: 5,
Compress: true,
}
logger := slog.New(slog.NewJSONHandler(r, nil))
slog.SetDefault(logger)
十二、多路输出
slog 的 Handler 是可组合和可扩展的,但不同 Handler 需要不同格式时,不能简单使用 io.MultiWriter。
例如:
Console → TextHandler
File → JSONHandler
这时需要实现 fan-out Handler,让同一个 Record 分别交给多个 Handler。
如果只是:
stdout → JSON
file → JSON
则可以考虑通过 Writer 层进行复制。
生产环境通常不建议应用同时维护 Console + File 两套日志。优先考虑:
Application → stdout → Log Collector
只有在明确的基础设施需求下才让应用直接写文件。
十三、敏感信息必须脱敏
结构化日志最大的优势也是风险来源。
因为业务代码可以非常方便地记录:
logger.Info("request",
"user", user,
)
如果 user 中包含:
password
token
authorization
cookie
email
phone
身份证号
银行卡号
就可能造成敏感数据泄露。
因此生产系统应建立统一日志规范:
禁止记录:
password
access_token
refresh_token
Authorization
Cookie
完整银行卡号
完整身份证号
允许记录:
user_id
request_id
trace_id
脱敏后的手机号
脱敏后的邮箱
必要时可以通过 ReplaceAttr 做统一脱敏,但最优方案仍然是业务代码从源头避免记录敏感数据。
十四、日志应该服务于可观测性
不要把日志当成“程序运行过程的文字记录”。
现代服务中,日志应该与:
Metrics
Tracing
Logs
共同构成 Observability。
例如一个 HTTP 请求:
trace_id
request_id
service
method
path
status
latency
user_id
error
可以让日志与 Trace、Metrics 互相关联。
推荐形成统一 Schema:
{
"time": "...",
"level": "INFO",
"service": "order",
"trace_id": "xxx",
"request_id": "xxx",
"method": "POST",
"path": "/orders",
"status": 200,
"latency": 35,
"msg": "request completed"
}
这样日志系统才能真正成为可查询的数据源,而不是简单的文本文件。
十五、与 zap / logrus 的关系
如果是新项目,我倾向于 slog,主要原因不是 slog 一定比 zap、zerolog 更快,而是:
- 标准库
- API 稳定
- 第三方依赖更少
- Go 生态逐渐围绕
slog建立统一接口 - Handler 可以扩展
- 方便替换输出实现
如果现有项目已经大量使用 zap,没有必要为了 slog 强行重写。
更合理的迁移方式是:
旧代码
↓
继续使用 zap
↓
新代码使用 slog
↓
逐步迁移
↓
最终统一
是否迁移应该考虑实际收益,而不是单纯追求“全部标准库化”。
十六、推荐的团队标准
如果让我给一个 Go Web 服务制定日志规范,我会采用下面这套基线:
- 日志库:统一使用标准库
log/slog。 - 输出格式:生产环境统一使用 JSON;时间、level、source、msg 必备;加请求上下文但禁止明文打敏感信息(用 LogValuer 脱敏更高效)。
- 输出位置:容器环境统一输出到
stdout/stderr,不由应用负责日志轮转(万一必须打到日志文件,必须配轮转 + 清理,防磁盘打满)。 - 默认级别:生产环境默认使用
Info,Debug默认关闭。 - 日志级别:合理使用
Debug、Info、Warn、Error,不要滥用Error。 - 公共字段:使用
Logger.With()注入service、version、env等公共字段。 - 结构化字段:优先使用结构化属性,不要拼接日志字符串。
- 嵌套字段:复杂对象使用
slog.Group()组织。 - 请求上下文:使用
InfoContext()、ErrorContext()等 API 关联context.Context。 - TraceID:通过 Context + 自定义 Handler 自动注入
trace_id。 - RequestID:需要时统一注入
request_id,避免各业务模块自行生成。 - 错误字段:统一使用
err表示错误,保持日志 Schema 一致。
在错误日志里塞一些结构化的、有业务语义的 key-value(比如业务模块 biz_module、错误类型 error_type 等),让监控系统能自动识别、分类、统计和告警,而不是只给人肉眼去翻日志。 - 敏感信息:禁止记录密码、Token、Cookie、Authorization 等敏感信息。
- 业务日志:避免业务代码直接依赖全局
slog,优先通过依赖注入传递*slog.Logger。 - 高频日志:性能敏感场景可以使用
LogAttrs(),是否有收益以 Benchmark 为准。 - 动态级别:需要线上动态调整日志级别时使用
slog.LevelVar。 - 日志轮转:Kubernetes 等容器环境交给日志基础设施;裸机使用
logrotate等工具。 - 多路输出:只有存在明确需求时才实现多 Handler 输出,避免应用同时维护 Console/File 两套日志。
- 可观测性:日志应与 Metrics、Tracing 通过
trace_id、request_id等字段关联。 - 日志 Schema:统一字段命名,不允许不同模块随意定义
error、err、exception等同义字段。 - 性能原则:不要为了理论上的日志性能优化牺牲业务代码可读性,先 Benchmark,再优化。
十七、结论
slog 的核心价值并不是“又一个日志库”,而是为 Go 建立了一套标准化的结构化日志抽象。
真正值得采用的不是简单地把 log.Printf 替换成 slog.Info,而是建立完整的日志工程体系:
结构化 Schema
+
统一字段
+
Context / Trace
+
统一 Handler
+
动态 Level
+
基础设施采集
+
日志脱敏
+
Metrics / Tracing
对于绝大多数 Go 后端服务,我的建议是:
优先使用 slog + JSON + stdout + Context/TraceID + Logger 依赖注入。
只有在明确存在性能、生态或特殊 Handler 需求时,再考虑引入 zap、zerolog 等第三方日志库。