引言
Go 1.21 引入了 log/slog 包,为 Go 语言带来了原生的结构化日志解决方案。与传统的简单日志包或第三方库相比,slog 提供了更强大、更灵活的日志记录能力。本文将深入探讨如何使用 slog 实现高效、可靠的日志记录。
核心概念
slog 围绕三个核心类型构建:
- Logger - 用户交互的前端接口
- Handler - 实际处理日志的后端
- Record - 在前后端之间传递的日志数据
go
// 创建使用 JSON 格式的 Logger
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// 创建记录并传递给 Handler
logger.Info("user logged in", "user_id", 123)
输出结果:
json
{
"time": "...",
"level": "INFO",
"msg": "user logged in",
"user_id": 123
}
最佳实践
1. 使用强类型属性
避免使用易出错的键值对方式:
go
// 容易出错 - 缺少值时会产生 !BADKEY
logger.Warn("permission denied", "user_id", 12345, "resource")
推荐使用类型安全的 slog.Attr:
go
logger.Warn("permission denied",
slog.Int("user_id", 12345),
slog.String("resource", "/api/admin"))
2. 使用 linter 强制一致性
通过 sloglint 确保代码库中的日志风格一致:
bash
# 安装 sloglint
go install github.com/ettle/strcase/cmd/sloglint@latest
# 运行检查
sloglint ./...
配置 .sloglintrc 文件来定义团队规范:
yaml
# 强制使用 slog.Attr 而非原始键值对
require-typed-attrs: true
# 要求所有日志消息使用小写
lowercase-messages: true
# 禁止在日志中记录敏感信息
forbidden-keys: ["password", "token", "secret"]
3. 合理设置日志级别
根据日志的重要性选择合适的级别:
go
// DEBUG - 详细调试信息,仅在开发/调试时启用
logger.Debug("database query executed", slog.String("query", sql))
// INFO - 常规操作信息
logger.Info("user created", slog.Int("user_id", userID))
// WARN - 异常情况但不影响系统运行
logger.Warn("cache miss", slog.String("key", cacheKey))
// ERROR - 错误情况需要关注
logger.Error("database connection failed", slog.String("error", err.Error()))
// Custom levels for business logic
const (
LevelTrace = slog.Level(-8)
LevelFatal = slog.Level(12)
)
4. 全局日志器配置
在应用启动时配置全局日志器:
go
func init() {
opts := &slog.HandlerOptions{
Level: slog.LevelInfo, // 生产环境通常使用 Info 级别
AddSource: false, // 生产环境关闭源码位置以提高性能
}
// 根据环境选择处理器
var handler slog.Handler
if os.Getenv("ENV") == "development" {
handler = slog.NewTextHandler(os.Stdout, opts)
} else {
handler = slog.NewJSONHandler(os.Stdout, opts)
}
slog.SetDefault(slog.New(handler))
}
5. 上下文传递日志器
在 HTTP 处理器或服务方法中传递带有上下文信息的日志器:
go
func handleRequest(w http.ResponseWriter, r *http.Request) {
// 为当前请求创建带上下文的日志器
requestLogger := slog.With(
slog.String("request_id", getRequestID(r)),
slog.String("user_agent", r.UserAgent()),
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
)
// 在处理逻辑中使用上下文日志器
if err := processRequest(r, requestLogger); err != nil {
requestLogger.Error("request processing failed", slog.String("error", err.Error()))
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
requestLogger.Info("request processed successfully")
}
6. 敏感信息处理
永远不要在日志中记录敏感信息:
go
// ❌ 错误做法
logger.Info("user login",
slog.String("email", user.Email),
slog.String("password", user.Password)) // 危险!
// ✅ 正确做法
logger.Info("user login",
slog.String("email", maskEmail(user.Email)),
slog.Bool("success", loginSuccess))
func maskEmail(email string) string {
parts := strings.Split(email, "@")
if len(parts) != 2 {
return "invalid-email"
}
username := parts[0]
if len(username) <= 2 {
return "**@" + parts[1]
}
masked := username[:2] + strings.Repeat("*", len(username)-2)
return masked + "@" + parts[1]
}
7. 性能优化
避免在日志记录中执行昂贵操作:
go
// ❌ 低效 - 即使日志级别不够也会执行序列化
logger.Debug("expensive data", slog.Any("data", expensiveSerialization()))
// ✅ 高效 - 使用惰性求值
if logger.Enabled(context.Background(), slog.LevelDebug) {
logger.Debug("expensive data", slog.Any("data", expensiveSerialization()))
}
// 或者使用 slog.Group 进行批量属性处理
logger.Info("user profile",
slog.Group("profile",
slog.String("name", user.Name),
slog.Int("age", user.Age),
slog.String("city", user.City),
))
8. 自定义 Handler
为特殊需求创建自定义 Handler:
go
type FilteringHandler struct {
slog.Handler
filter func(slog.Record) bool
}
func (h *FilteringHandler) Handle(ctx context.Context, r slog.Record) error {
if h.filter(r) {
return h.Handler.Handle(ctx, r)
}
return nil
}
// 使用示例:过滤掉包含特定关键词的日志
filterHandler := &FilteringHandler{
Handler: slog.NewJSONHandler(os.Stdout, nil),
filter: func(r slog.Record) bool {
return !strings.Contains(r.Message, "heartbeat")
},
}
logger := slog.New(filterHandler)
9. 结构化错误处理
将错误信息结构化记录:
go
// 自定义错误类型
type AppError struct {
Code string
Message string
Cause error
}
func (e *AppError) Error() string {
return e.Message
}
// 记录结构化错误
if err != nil {
var appErr *AppError
if errors.As(err, &appErr) {
logger.Error("application error",
slog.String("error_code", appErr.Code),
slog.String("error_message", appErr.Message),
slog.String("cause", appErr.Cause.Error()))
} else {
logger.Error("unexpected error", slog.String("error", err.Error()))
}
}
10. 测试日志输出
在单元测试中验证日志行为:
go
func TestUserService_CreateUser(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewJSONHandler(&buf, nil))
service := NewUserService(logger)
err := service.CreateUser(context.Background(), "test@example.com")
assert.NoError(t, err)
// 验证日志内容
var logRecord map[string]interface{}
err = json.Unmarshal(buf.Bytes(), &logRecord)
assert.NoError(t, err)
assert.Equal(t, "user created", logRecord["msg"])
assert.Equal(t, "test@example.com", logRecord["email"])
}
总结
slog 包为 Go 应用提供了现代化的结构化日志解决方案。通过遵循这些最佳实践,你可以构建出既高效又可靠的日志系统,为应用的可观测性和故障排查提供有力支持。好的日志实践不仅能帮助你快速定位问题,还能在不影响性能的前提下提供丰富的上下文信息。