大家好,我是极客老墨。

排查问题时,日志是第一手段。服务挂了、请求慢了、数据错了,都要靠日志定位。但日志写不好,要么找不到关键信息,要么性能拖累整个系统。

这篇就聊聊 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

方案选择

对比表

特性logslogzap
性能
结构化
日志级别
学习成本
官方支持
适用场景简单脚本一般应用高性能服务

选择建议

  • 简单脚本:使用标准库 log
  • 一般应用:使用 slog(Go 1.21+)
  • 高性能服务:使用 zap
  • 已有项目:根据现状选择,不必强制迁移

老墨总结

Go 日志管理的 5 个关键点:

  1. 结构化日志:使用键值对而不是字符串拼接,便于检索和分析
  2. 日志级别:Debug 开发用,Info 生产用,合理设置避免日志过多
  3. 上下文信息:添加 request_id、user_id 等,便于追踪问题
  4. 日志轮转:使用 lumberjack 避免日志文件过大
  5. 性能考虑:高频日志使用 zap,低频日志用 slog 或标准库

实战建议

  • 生产环境使用结构化日志(slog 或 zap)
  • 不要记录敏感信息(密码、Token)
  • 使用 defer logger.Sync() 确保日志写入
  • 提供 HTTP 接口动态调整日志级别
  • 定期清理旧日志文件

好的日志系统能大大提高问题排查效率,值得投入时间优化。


你在项目中用什么日志方案?遇到过什么日志相关的问题?欢迎评论区聊聊。

极客老墨,继续折腾!

练习题

  1. 使用标准库 log 实现日志同时输出到文件和控制台
  2. 使用 slog 实现带有 request_id 的 HTTP 请求日志
  3. 使用 zap 实现日志轮转,单个文件最大 50MB,保留 7 天
  4. 实现一个日志中间件,记录 HTTP 请求的耗时和状态码
  5. 使用 zap 实现动态调整日志级别的 HTTP 接口
  6. 对比 log、slog、zap 的性能,编写基准测试

相关阅读