Go从入门到精通(22) - 一个简单web项目-统一日志输出
Go从入门到精通(21) - 一个简单web项目-统一日志输出
统一日志输出
文章目录
- Go从入门到精通(21) - 一个简单web项目-统一日志输出
- 前言
- 日志库横向对比
- zap 使用
- 安装依赖
- 创建日志配置
- 修改主程序的日志
- 在处理函数中使用日志
- 日志示例
- 控制台输出
- 文件输出(json)
- Logger 和 SugaredLogger
前言
在 Go 语言中选择日志库时,需要结合项目规模、性能需求、功能复杂度以及是否需要结构化日志等因素综合考量。
日志库横向对比
特性 | log/slog(标准库) | Zap(Uber) | Zerolog | Logrus |
---|---|---|---|---|
项目背景 | Go 官方(1.21+ 内置) | Uber 开源,工业级实践 | 社区开源,专注极致性能 | 早期主流,社区维护(功能冻结) |
结构化日志 | 支持(key-value 原生) | 支持(Logger 结构化,SugaredLogger 兼容非结构化) | 强制结构化(JSON 输出,流式 API) | 支持(字段扩展) |
日志级别 | Debug/Info/Warn/Error 四级 | Debug/Info/Warn/Error/Dpanic/Panic/Fatal 七级 | Debug/Info/Warn/Error/Fatal 五级 | Debug/Info/Warn/Error/Fatal/Panic 六级 |
性能(写入速度) | 中(原生实现,无过度优化) | 极高(预分配内存,非反射序列化) | 极高(零内存分配,流式构建) | 中低(反射序列化,内存分配较多) |
API 风格 | 类似标准库,简洁直观 | 结构化需显式类型(如 Int、String),Sugared 兼容 fmt 风格 | 链式调用(如 log.Info().Str(“k”,“v”).Msg(“”)) | 类似 fmt,支持 WithFields 扩展 |
动态级别调整 | 需自定义 Handler 实现(第三方支持) | 原生支持(通过 AtomicLevel) | 支持(Level 接口) | 支持(需手动实现或依赖插件) |
日志轮转 | 需依赖第三方 Handler(如 github.com/lmittmann/tint) | 原生支持(结合 lumberjack 等) | 需依赖第三方(如 github.com/rs/zerolog/logrotate) | 需依赖插件(如 github.com/lestrrat-go/file-rotatelogs) |
内存分配 | 较少(原生优化) | 极少(预分配+非反射) | 几乎零分配(流式构建+栈上操作) | 较多(反射+动态字段) |
扩展能力 | 强(Handler 接口可自定义) | 强(Core 接口+大量第三方集成) | 中(输出适配器扩展) | 强(Hooks 机制+丰富插件) |
学习成本 | 低(官方文档完善,类似标准库) | 中(结构化 API 稍繁琐,Sugared 降低门槛) | 中(链式 API 需适应) | 低(类似 fmt,文档丰富) |
依赖情况 | 无(标准库内置) | 无(纯 Go 实现,无额外依赖) | 无(纯 Go 实现) | 无(纯 Go 实现) |
适用场景 | 新项目首选、减少依赖、基础结构化需求 | 高并发服务、性能敏感场景、功能全面需求 | 内存敏感场景(如嵌入式)、纯结构化日志需求 | 旧项目兼容、依赖生态插件的场景 |
优势 | 官方维护、稳定性强、无依赖、长期兼容 | 性能顶尖、功能全面、结构化+非结构化双模式 | 零内存分配、极简设计、严格结构化 | 生态成熟、迁移成本低、插件丰富 |
不足 | 高级功能需第三方扩展(如异步写入) | 结构化 API 稍繁琐(可通过 Sugared 规避) | 不支持非结构化日志,灵活性有限 | 性能一般,功能不再更新(仅维护) |
- 新项目首选:slog(官方稳定)或 Zap(性能强、功能全)。
- 性能敏感场景:Zap 或 Zerolog。
- 兼容性 / 旧项目:Logrus(短期)或迁移到 slog/Zap(长期)。
zap 使用
这里主要介绍zap使用,接入我们之前的项目
安装依赖
go get -u go.uber.org/zap
go get -u go.uber.org/zap/zapcore
创建日志配置
// logger/logger.go
package loggerimport ("go.uber.org/zap""go.uber.org/zap/zapcore""gopkg.in/natefinch/lumberjack.v2""os""time"
)var Logger *zap.Logger
var Sugar *zap.SugaredLoggerfunc init() {var err error// 配置编码器encoderConfig := zapcore.EncoderConfig{TimeKey: "ts",LevelKey: "level",NameKey: "logger",CallerKey: "caller",MessageKey: "msg",StacktraceKey: "stacktrace",LineEnding: zapcore.DefaultLineEnding,EncodeLevel: zapcore.CapitalLevelEncoder,EncodeTime: zapcore.ISO8601TimeEncoder,EncodeDuration: zapcore.SecondsDurationEncoder,EncodeCaller: zapcore.ShortCallerEncoder,}// 确定日志级别level := zap.InfoLevelif os.Getenv("ENV") == "development" {level = zap.DebugLevel}// 创建日志目录logDir := "./logs"if _, err := os.Stat(logDir); os.IsNotExist(err) {if err := os.MkdirAll(logDir, 0755); err != nil {panic(fmt.Sprintf("无法创建日志目录: %v", err))}}// 配置文件写入器(使用 lumberjack 实现日志切割)fileWriter := zapcore.AddSync(&lumberjack.Logger{Filename: logDir + "/app.log",MaxSize: 10, // 每个日志文件最大 10MBMaxBackups: 30, // 最多保留 30 个备份MaxAge: 7, // 最多保留 7 天Compress: true, // 压缩旧日志})// 配置控制台写入器consoleWriter := zapcore.Lock(os.Stdout)// 创建核心core := zapcore.NewTee(zapcore.NewCore(zapcore.NewJSONEncoder(encoderConfig),fileWriter,level,),zapcore.NewCore(zapcore.NewConsoleEncoder(encoderConfig),consoleWriter,level,),)// 创建 LoggerLogger = zap.New(core, zap.AddCaller(),zap.AddStacktrace(zap.ErrorLevel),zap.Fields(zap.String("service", "user-api")),)// 创建 SugaredLogger(提供更灵活的日志方法)Sugar = Logger.Sugar()// 确保程序退出时刷新日志defer Logger.Sync()Sugar.Infow("日志系统初始化完成", "level", level.String())
}
修改主程序的日志
app.go
package appimport ("github.com/gin-contrib/cors""github.com/gin-gonic/gin"swaggerFiles "github.com/swaggo/files"ginSwagger "github.com/swaggo/gin-swagger""go-web-demo/app/api""go-web-demo/app/utils""go-web-demo/docs""go-web-demo/logger""os"
)func StartApp() error {// 设置为生产模式if os.Getenv("ENV") == "production" {gin.SetMode(gin.ReleaseMode)}// 创建默认引擎,包含日志和恢复中间件router := gin.Default()// 添加自定义中间件:请求日志router.Use(utils.LoggingMiddleware())// 配置CORSrouter.Use(cors.Default())// Swagger文档路由docs.Init("localhost:8082")router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))err := api.InitRouters(router)if err != nil {return err}// 启动服务器port := os.Getenv("PORT")if port == "" {port = "8082"}logger.Sugar.Infow("服务器启动成功", "port", port, "env", os.Getenv("ENV"))if err := router.Run(":" + port); err != nil {logger.Sugar.Fatalw("服务器启动失败", "error", err)}return nil
}
token_utils.go
// 自定义日志中间件
func LoggingMiddleware() gin.HandlerFunc {return func(c *gin.Context) {start := time.Now()// 记录请求信息logger.Sugar.Infow("收到请求","method", c.Request.Method,"path", c.Request.URL.Path,"query", c.Request.URL.RawQuery,"client_ip", c.ClientIP(),"user_agent", c.Request.UserAgent(),)// 处理请求c.Next()// 记录响应信息duration := time.Since(start)logger.Sugar.Infow("请求处理完成","status", c.Writer.Status(),"latency", duration.Seconds(),"bytes", c.Writer.Size(),)}
}
在处理函数中使用日志
// RegisterHandler 注册新用户
func RegisterHandler(c *gin.Context) {var request RegisterRequest// 绑定并验证请求if err := c.ShouldBindJSON(&request); err != nil {logger.Sugar.Warnw("无效请求参数", "error", err.Error(),"body", c.Request.Body,)c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}// 检查用户名是否已存在for _, user := range users {if user.Username == request.Username {logger.Sugar.Warnw("用户名已存在", "username", request.Username)logger.Logger.Warn("用户名已存在", zap.String("username", request.Username))c.JSON(http.StatusConflict, gin.H{"error": "Username already exists"})return}}// 哈希密码hashedPassword, err := bcrypt.GenerateFromPassword([]byte(request.Password), bcrypt.DefaultCost)if err != nil {logger.Sugar.Errorw("密码哈希失败", "error", err)c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})return}// 创建新用户userID := fmt.Sprintf("%d", nextUserID)nextUserID++user := User{ID: userID,Username: request.Username,Password: string(hashedPassword),Email: request.Email,}// 保存用户users[userID] = user// 生成令牌token, err := generateToken(userID)if err != nil {logger.Sugar.Errorw("生成令牌失败", "error", err)c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})return}logger.Sugar.Infow("用户注册成功", "user_id", userID,"username", request.Username,)c.JSON(http.StatusCreated, TokenResponse{Token: token})
}// 其他处理函数类似...
日志示例
控制台输出
2023-07-15T14:30:00.123+0800 INFO logger/logger.go:65 日志系统初始化完成 {“service”: “user-api”, “level”: “debug”}
2023-07-15T14:30:01.456+0800 INFO main.go:78 服务器启动成功 {“service”: “user-api”, “port”: “8080”, “env”: “development”}
2023-07-15T14:30:05.789+0800 INFO main.go:95 收到请求 {“service”: “user-api”, “method”: “POST”, “path”: “/api/register”, “query”: “”, “client_ip”: “127.0.0.1”, “user_agent”: “curl/7.68.0”}
2023-07-15T14:30:05.901+0800 INFO main.go:104 请求处理完成 {“service”: “user-api”, “status”: 201, “latency”: 0.112, “bytes”: 123}
文件输出(json)
{“ts”:“2023-07-15T14:30:05.789+0800”,“level”:“INFO”,“logger”:“user-api”,“caller”:“main.go:95”,“msg”:“收到请求”,“method”:“POST”,“path”:“/api/register”,“query”:“”,“client_ip”:“127.0.0.1”,“user_agent”:“curl/7.68.0”}
{“ts”:“2023-07-15T14:30:05.901+0800”,“level”:“INFO”,“logger”:“user-api”,“caller”:“main.go:104”,“msg”:“请求处理完成”,“status”:201,“latency”:0.112,“bytes”:123}
这种日志配置既满足开发环境的可读性需求,又适合生产环境的日志收集和分析系统(如ELK)。
Logger 和 SugaredLogger
- 性能差异
Logger:
- 使用类型安全的方法(如 zap.String(key, value)、zap.Int(key, value)),避免反射。
- 日志构建过程中几乎无内存分配,适合高频调用的关键路径(如 API 处理、循环内部)。
SugaredLogger:
- 使用 interface{} 类型接收参数,运行时通过反射推断类型,性能略低。
- 适合低频调用的非关键路径(如初始化日志、异常处理)。
- API 风格差异
每条日志必须显式指定键值对及其类型,确保日志格式统一。
logger.Info("http request processed",zap.String("method", "POST"),zap.Int("status", 200),zap.Duration("elapsed", time.Since(start)),
)
输出结果(JSON 格式):
{
“level”: “info”,
“ts”: 1680000000.123,
“caller”: “main.go:42”,
“msg”: “http request processed”,
“method”: “POST”,
“status”: 200,
“elapsed”: “500.5µs”
}
SugaredLogger(非结构化):
使用类似 fmt.Sprintf 的风格,支持占位符和任意类型参数。
sugar.Info("http request processed: %s %d (%s)","POST", 200, time.Since(start),
)
输出结果(JSON 格式):
{
“level”: “info”,
“ts”: 1680000000.123,
“caller”: “main.go:42”,
“msg”: “http request processed: POST 200 (500.5µs)”
}
- 适用场景
- Logger:
- 生产环境的核心服务(如 API 网关、数据库操作)。
- 需要精确控制日志格式和性能的场景。
- 日志会被 ELK、Prometheus 等系统收集分析(结构化数据更易处理)。
- SugaredLogger:
- 开发阶段的快速调试(如打印临时变量)。
- 日志格式灵活性要求高的场景(如输出复杂对象)。
- 非关键路径的低频日志(如配置加载、启动信息)。
三、最佳实践
混合使用:
- 在性能敏感的代码中使用 Logger,在调试或非关键路径使用 SugaredLogger。
- 避免在循环中使用 SugaredLogger:
反射开销在高频调用时会显著影响性能。 - 生产环境优先使用 Logger:
结构化日志更易于自动化分析和监控告警。