应使用 zap 替代标准 log 包实现结构化日志:通过 lumberjack 轮转文件,按环境动态配置输出格式(dev 用 Development,prod 用 Production),显式 Sync() 避免丢失日志,禁止裸 print,敏感字段需脱敏。

用 log 包做基础统一输出,但得绕过默认的 os.Stderr
Go 标准库的 log 包默认写到 os.Stderr,没法直接切到文件或加结构化字段。想统一输出格式和目标,必须用 log.SetOutput() 和 log.SetFlags() 手动接管。
-
log.SetOutput(os.Stdout)或log.SetOutput(f *os.File)可切换输出目标,比如日志文件 -
log.SetFlags(log.LstdFlags | log.Lshortfile)能加时间戳和文件行号,避免所有日志都挤在一行没上下文 - 别直接用
log.Println()混合调试和业务日志——建议封装一层Info()/Error()函数,统一加前缀(如[app])
用 zap 实现高性能结构化日志
标准 log 包不支持结构化字段(如 {"user_id": 123, "action": "login"}),而生产环境需要快速过滤和采集。Zap 是 Go 生态事实标准,zap.SugaredLogger 适合开发,zap.Logger 更快但 API 稍重。
- 初始化时用
zap.NewProduction()获取预设 JSON 输出、带调用栈、自动轮转的 logger;开发用zap.NewDevelopment()更易读 - 写日志别用字符串拼接:
logger.Info("user login", zap.String("user_id", "u_abc"), zap.Int("status_code", 200))—— 字段名和值分开传,才能被 ELK 正确解析 - 注意
defer logger.Sync():Zap 是异步写入,进程退出前不显式Sync(),最后一段日志大概率丢失
按环境动态切换日志配置(dev/staging/prod)
开发时要可读,线上要可采集,不能靠改代码。推荐用环境变量控制,比如 ENV=prod 触发 JSON + 文件输出,ENV=dev 则彩色控制台 + 行号。
- 用
os.Getenv("ENV")读取,别硬编码判断字符串,方便 CI 注入 - 文件输出需配
lumberjack.Logger做轮转:Filename、MaxSize(单位 MB)、MaxBackups必须设,否则单个日志文件会无限增长 - 结构化日志里别打敏感字段:
zap.String("password", "***")或直接过滤掉password、token类 key,避免误入日志系统
避免 fmt.Printf 和裸 println 混入日志流
这类调用绕过所有日志配置,既不会进文件,也不会带时间戳,更不会被集中采集。CI/CD 流水线里一旦出现,等于埋了个监控盲点。
立即学习“go语言免费学习笔记(深入)”;
- 全局搜
fmt\.Print和println\(,替换成封装好的logger.Debug()或log.Printf() - 用
go vet -printf静态检查能发现部分裸打印,但无法覆盖所有情况,得靠 Code Review 卡住 - 测试代码里允许用
t.Log(),但正式 handler 或 service 层绝对禁止出现fmt包输出
package main
import (
"os"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
)
func newLogger() (*zap.Logger, error) {
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
core := zapcore.NewCore(
zapcore.NewJSONEncoder(encoderConfig),
&lumberjack.Logger{
Filename: "app.log",
MaxSize: 10,
MaxBackups: 5,
MaxAge: 28,
},
zapcore.InfoLevel,
)
return zap.New(core), nil
}
Zap 的 core 构建过程容易漏掉 lumberjack 的 MaxAge 或写错 MaxSize 单位(是 MB 不是 KB),这些参数不生效时,日志文件会悄无声息地撑爆磁盘。










