大家好,我是极客老墨。
排查问题时,日志是第一手段。服务挂了、请求慢了、数据错了,都要靠日志定位。但日志写不好,要么找不到关键信息,要么性能拖累整个系统。
这篇就聊聊 Go 的日志管理,从标准库到高性能方案,看看怎么写好日志。
标准库 log
Go 自带的 log 包,简单但功能有限。
基本使用
1package main
2
3import (
4 "log"
5)
6
7func main() {
8 // 基本输出
9 log.Println("Server started")
10
11 // 格式化输出
12 log.Printf("User %s logged in", "admin")
13
14 // 带前缀
15 log.SetPrefix("[MyApp] ")
16 log.Println("With prefix")
17
18 // 输出:[MyApp] 2025/07/18 10:00:00 With prefix
19}
要点:
log.Println输出日志log.Printf格式化输出log.SetPrefix设置前缀- 默认输出到 stderr
自定义 Logger
1import (
2 "log"
3 "os"
4)
5
6func main() {
7 // 创建自定义 Logger
8 logger := log.New(
9 os.Stdout, // 输出目标
10 "[INFO] ", // 前缀
11 log.Ldate|log.Ltime|log.Lshortfile, // 标志
12 )
13
14 logger.Println("Custom logger message")
15 // 输出:[INFO] 2025/07/18 10:00:00 main.go:15: Custom logger message
16}
标志选项:
log.Ldate:日期(2025/07/18)log.Ltime:时间(10:00:00)log.Lmicroseconds:微秒log.Llongfile:完整文件名和行号log.Lshortfile:文件名和行号log.LUTC:使用 UTC 时间
写入文件
1func main() {
2 // 打开日志文件
3 file, err := os.OpenFile("app.log",
4 os.O_CREATE|os.O_WRONLY|os.O_APPEND,
5 0666)
6 if err != nil {
7 log.Fatal(err)
8 }
9 defer file.Close()
10
11 // 设置输出到文件
12 log.SetOutput(file)
13
14 log.Println("This goes to file")
15}
Fatal 和 Panic
1func main() {
2 // Fatal:输出日志后调用 os.Exit(1)
3 // log.Fatal("Fatal error")
4
5 // Panic:输出日志后触发 panic
6 // log.Panic("Panic error")
7
8 // 正常日志
9 log.Println("Normal log")
10}
⚠️ 注意:
Fatal会终止程序,defer 不会执行Panic会触发 panic,可以被 recover- 生产环境谨慎使用
标准库的局限
- ❌ 不支持日志级别(Debug、Info、Error)
- ❌ 不支持结构化日志
- ❌ 性能一般
- ❌ 功能简单
slog:结构化日志
Go 1.21 引入的官方结构化日志库。
基本使用
1import (
2 "log/slog"
3 "os"
4)
5
6func main() {
7 // 创建 JSON 格式的 Logger
8 logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
9
10 logger.Info("User logged in",
11 "user_id", 123,
12 "username", "admin",
13 "ip", "192.168.1.1")
14
15 // 输出:
16 // {"time":"2025-07-18T10:00:00Z","level":"INFO","msg":"User logged in","user_id":123,"username":"admin","ip":"192.168.1.1"}
17}
要点:
- 键值对形式记录日志
- 支持 JSON 和文本格式
- 便于日志检索和分析
日志级别
1func main() {
2 // 创建 Text 格式的 Logger
3 logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
4 Level: slog.LevelInfo, // 只输出 Info 及以上级别
5 }))
6
7 logger.Debug("Debug message") // 不会输出
8 logger.Info("Info message") // 会输出
9 logger.Warn("Warning message") // 会输出
10 logger.Error("Error message") // 会输出
11}
日志级别(从低到高):
Debug:调试信息Info:一般信息Warn:警告信息Error:错误信息
上下文日志
1func main() {
2 logger := slog.Default()
3
4 // 创建带有固定字段的子 Logger
5 requestLogger := logger.With(
6 "request_id", "abc123",
7 "user_id", 456,
8 )
9
10 requestLogger.Info("Processing request")
11 requestLogger.Info("Request completed")
12
13 // 两条日志都会自动包含 request_id 和 user_id
14}
使用场景:
- HTTP 请求处理
- 用户会话跟踪
- 分布式追踪
分组字段
1func main() {
2 logger := slog.Default()
3
4 logger.Info("User action",
5 slog.Group("user",
6 slog.String("id", "123"),
7 slog.String("name", "admin"),
8 ),
9 slog.Group("action",
10 slog.String("type", "login"),
11 slog.String("ip", "192.168.1.1"),
12 ),
13 )
14
15 // 输出:
16 // {"time":"...","level":"INFO","msg":"User action","user":{"id":"123","name":"admin"},"action":{"type":"login","ip":"192.168.1.1"}}
17}
自定义 Handler
1type CustomHandler struct {
2 slog.Handler
3}
4
5func (h *CustomHandler) Handle(ctx context.Context, r slog.Record) error {
6 // 自定义处理逻辑
7 // 例如:过滤敏感信息、添加额外字段等
8 return h.Handler.Handle(ctx, r)
9}
10
11func main() {
12 handler := &CustomHandler{
13 Handler: slog.NewJSONHandler(os.Stdout, nil),
14 }
15
16 logger := slog.New(handler)
17 logger.Info("Custom handler")
18}
zap:高性能日志
Uber 开源的高性能日志库。
安装
1go get -u go.uber.org/zap
快速开始
1import (
2 "go.uber.org/zap"
3)
4
5func main() {
6 // 开发环境:易读的格式
7 logger, _ := zap.NewDevelopment()
8 defer logger.Sync() // 刷新缓冲区
9
10 logger.Info("User logged in",
11 zap.String("username", "admin"),
12 zap.Int("user_id", 123),
13 zap.String("ip", "192.168.1.1"))
14
15 // 输出:
16 // 2025-07-18T10:00:00.123+0800 INFO User logged in {"username": "admin", "user_id": 123, "ip": "192.168.1.1"}
17}
要点:
NewDevelopment开发环境配置NewProduction生产环境配置Sync()刷新缓冲区,确保日志写入
生产环境配置
1func main() {
2 // 生产环境:JSON 格式
3 logger, _ := zap.NewProduction()
4 defer logger.Sync()
5
6 logger.Info("Server started",
7 zap.String("port", "8080"),
8 zap.String("env", "production"))
9
10 // 输出:
11 // {"level":"info","ts":1689667200.123,"caller":"main.go:10","msg":"Server started","port":"8080","env":"production"}
12}
自定义配置
1func main() {
2 config := zap.NewProductionConfig()
3
4 // 设置日志级别
5 config.Level = zap.NewAtomicLevelAt(zap.InfoLevel)
6
7 // 设置输出路径
8 config.OutputPaths = []string{
9 "stdout",
10 "./logs/app.log",
11 }
12
13 // 设置错误日志路径
14 config.ErrorOutputPaths = []string{
15 "stderr",
16 "./logs/error.log",
17 }
18
19 // 设置编码格式
20 config.Encoding = "json"
21
22 logger, _ := config.Build()
23 defer logger.Sync()
24
25 logger.Info("Application started")
26}
SugaredLogger
牺牲一点性能换取更简洁的 API。
1func main() {
2 logger, _ := zap.NewProduction()
3 defer logger.Sync()
4
5 // 结构化日志(最快)
6 logger.Info("User logged in",
7 zap.String("username", "admin"))
8
9 // SugaredLogger(稍慢,但更方便)
10 sugar := logger.Sugar()
11 sugar.Infof("User %s logged in", "admin")
12 sugar.Infow("User logged in",
13 "username", "admin",
14 "user_id", 123)
15}
选择建议:
- 高频日志:使用
logger - 低频日志:使用
sugar
字段类型
1func main() {
2 logger, _ := zap.NewProduction()
3 defer logger.Sync()
4
5 logger.Info("Various types",
6 zap.String("string", "value"),
7 zap.Int("int", 123),
8 zap.Int64("int64", 123456789),
9 zap.Float64("float", 3.14),
10 zap.Bool("bool", true),
11 zap.Duration("duration", time.Second),
12 zap.Time("time", time.Now()),
13 zap.Any("any", map[string]int{"a": 1}),
14 )
15}
日志轮转
使用 lumberjack 实现日志文件自动轮转。
安装
1go get -u gopkg.in/natefinch/lumberjack.v2
基本使用
1import (
2 "go.uber.org/zap"
3 "go.uber.org/zap/zapcore"
4 "gopkg.in/natefinch/lumberjack.v2"
5)
6
7func main() {
8 // 配置日志轮转
9 w := zapcore.AddSync(&lumberjack.Logger{
10 Filename: "./logs/app.log",
11 MaxSize: 100, // MB
12 MaxBackups: 3, // 保留旧文件数量
13 MaxAge: 28, // 天
14 Compress: true, // 压缩旧文件
15 })
16
17 core := zapcore.NewCore(
18 zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
19 w,
20 zap.InfoLevel,
21 )
22
23 logger := zap.New(core)
24 defer logger.Sync()
25
26 logger.Info("Log with rotation")
27}
配置说明:
Filename:日志文件路径MaxSize:单个文件最大大小(MB)MaxBackups:保留的旧文件数量MaxAge:保留的天数Compress:是否压缩旧文件
多输出
同时输出到文件和控制台。
1func main() {
2 // 文件输出
3 fileWriter := zapcore.AddSync(&lumberjack.Logger{
4 Filename: "./logs/app.log",
5 MaxSize: 100,
6 MaxBackups: 3,
7 MaxAge: 28,
8 Compress: true,
9 })
10
11 // 控制台输出
12 consoleWriter := zapcore.AddSync(os.Stdout)
13
14 // 组合多个输出
15 core := zapcore.NewTee(
16 zapcore.NewCore(
17 zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
18 fileWriter,
19 zap.InfoLevel,
20 ),
21 zapcore.NewCore(
22 zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()),
23 consoleWriter,
24 zap.DebugLevel,
25 ),
26 )
27
28 logger := zap.New(core)
29 defer logger.Sync()
30
31 logger.Debug("Debug message") // 只在控制台
32 logger.Info("Info message") // 文件和控制台都有
33}
动态调整日志级别
运行时调整日志级别,无需重启服务。
使用 AtomicLevel
1import (
2 "go.uber.org/zap"
3 "go.uber.org/zap/zapcore"
4)
5
6func main() {
7 // 创建可调整的日志级别
8 atom := zap.NewAtomicLevel()
9 atom.SetLevel(zap.InfoLevel)
10
11 config := zap.NewProductionConfig()
12 config.Level = atom
13
14 logger, _ := config.Build()
15 defer logger.Sync()
16
17 logger.Info("This will be logged")
18 logger.Debug("This won't be logged")
19
20 // 动态调整为 Debug 级别
21 atom.SetLevel(zapcore.DebugLevel)
22
23 logger.Debug("Now this will be logged")
24}
HTTP 接口调整
1import (
2 "net/http"
3 "go.uber.org/zap"
4)
5
6func main() {
7 atom := zap.NewAtomicLevel()
8
9 config := zap.NewProductionConfig()
10 config.Level = atom
11
12 logger, _ := config.Build()
13 defer logger.Sync()
14
15 // 提供 HTTP 接口调整日志级别
16 http.Handle("/log/level", atom)
17
18 go http.ListenAndServe(":8080", nil)
19
20 // 使用:
21 // curl -X PUT http://localhost:8080/log/level -d level=debug
22}
最佳实践
1. 合理设置日志级别
1// ✅ 好:根据环境设置
2func getLogLevel(env string) zapcore.Level {
3 switch env {
4 case "development":
5 return zap.DebugLevel
6 case "production":
7 return zap.InfoLevel
8 default:
9 return zap.InfoLevel
10 }
11}
12
13// ❌ 不好:生产环境使用 Debug
14logger, _ := zap.NewDevelopment() // 性能差,日志太多
2. 使用结构化日志
1// ✅ 好:结构化
2logger.Info("User logged in",
3 zap.String("user_id", "123"),
4 zap.String("username", "admin"))
5
6// ❌ 不好:字符串拼接
7logger.Info(fmt.Sprintf("User %s (ID: %s) logged in", "admin", "123"))
3. 避免敏感信息
1// ❌ 不好:记录密码
2logger.Info("User login",
3 zap.String("username", "admin"),
4 zap.String("password", "secret123")) // 危险!
5
6// ✅ 好:不记录敏感信息
7logger.Info("User login",
8 zap.String("username", "admin"))
4. 添加上下文信息
1// ✅ 好:添加 request_id
2func handleRequest(w http.ResponseWriter, r *http.Request) {
3 requestID := generateRequestID()
4 logger := logger.With(zap.String("request_id", requestID))
5
6 logger.Info("Request started")
7 // 处理请求
8 logger.Info("Request completed")
9}
5. 使用 defer Sync
1// ✅ 好:确保日志写入
2func main() {
3 logger, _ := zap.NewProduction()
4 defer logger.Sync() // 重要!
5
6 logger.Info("Application started")
7}
8
9// ❌ 不好:忘记 Sync
10func main() {
11 logger, _ := zap.NewProduction()
12 logger.Info("Application started")
13 // 程序退出时可能丢失日志
14}
性能对比
基准测试
1import (
2 "log"
3 "log/slog"
4 "testing"
5 "go.uber.org/zap"
6)
7
8func BenchmarkStdLog(b *testing.B) {
9 logger := log.New(io.Discard, "", log.LstdFlags)
10 b.ResetTimer()
11 for i := 0; i < b.N; i++ {
12 logger.Println("test message")
13 }
14}
15
16func BenchmarkSlog(b *testing.B) {
17 logger := slog.New(slog.NewJSONHandler(io.Discard, nil))
18 b.ResetTimer()
19 for i := 0; i < b.N; i++ {
20 logger.Info("test message")
21 }
22}
23
24func BenchmarkZap(b *testing.B) {
25 logger := zap.NewExample()
26 b.ResetTimer()
27 for i := 0; i < b.N; i++ {
28 logger.Info("test message")
29 }
30}
结果(仅供参考):
- 标准库 log:~1000 ns/op
- slog:~500 ns/op
- zap:~200 ns/op
方案选择
对比表
| 特性 | log | slog | zap |
|---|---|---|---|
| 性能 | 中 | 中 | 高 |
| 结构化 | ❌ | ✅ | ✅ |
| 日志级别 | ❌ | ✅ | ✅ |
| 学习成本 | 低 | 低 | 中 |
| 官方支持 | ✅ | ✅ | ❌ |
| 适用场景 | 简单脚本 | 一般应用 | 高性能服务 |
选择建议
- 简单脚本:使用标准库
log - 一般应用:使用
slog(Go 1.21+) - 高性能服务:使用
zap - 已有项目:根据现状选择,不必强制迁移
老墨总结
Go 日志管理的 5 个关键点:
- 结构化日志:使用键值对而不是字符串拼接,便于检索和分析
- 日志级别:Debug 开发用,Info 生产用,合理设置避免日志过多
- 上下文信息:添加 request_id、user_id 等,便于追踪问题
- 日志轮转:使用 lumberjack 避免日志文件过大
- 性能考虑:高频日志使用 zap,低频日志用 slog 或标准库
实战建议:
- 生产环境使用结构化日志(slog 或 zap)
- 不要记录敏感信息(密码、Token)
- 使用 defer logger.Sync() 确保日志写入
- 提供 HTTP 接口动态调整日志级别
- 定期清理旧日志文件
好的日志系统能大大提高问题排查效率,值得投入时间优化。
你在项目中用什么日志方案?遇到过什么日志相关的问题?欢迎评论区聊聊。
极客老墨,继续折腾!
练习题
- 使用标准库 log 实现日志同时输出到文件和控制台
- 使用 slog 实现带有 request_id 的 HTTP 请求日志
- 使用 zap 实现日志轮转,单个文件最大 50MB,保留 7 天
- 实现一个日志中间件,记录 HTTP 请求的耗时和状态码
- 使用 zap 实现动态调整日志级别的 HTTP 接口
- 对比 log、slog、zap 的性能,编写基准测试