当前位置: 首页 > news >正文

Go从入门到精通(22) - 一个简单web项目-统一日志输出

Go从入门到精通(21) - 一个简单web项目-统一日志输出

统一日志输出


文章目录

  • Go从入门到精通(21) - 一个简单web项目-统一日志输出
  • 前言
  • 日志库横向对比
  • zap 使用
    • 安装依赖
    • 创建日志配置
    • 修改主程序的日志
    • 在处理函数中使用日志
  • 日志示例
    • 控制台输出
    • 文件输出(json)
  • Logger 和 SugaredLogger


前言

在 Go 语言中选择日志库时,需要结合项目规模、性能需求、功能复杂度以及是否需要结构化日志等因素综合考量。


日志库横向对比

特性log/slog(标准库)Zap(Uber)ZerologLogrus
项目背景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

  1. 性能差异

Logger:

  • 使用类型安全的方法(如 zap.String(key, value)、zap.Int(key, value)),避免反射。
  • 日志构建过程中几乎无内存分配,适合高频调用的关键路径(如 API 处理、循环内部)。

SugaredLogger:

  • 使用 interface{} 类型接收参数,运行时通过反射推断类型,性能略低。
  • 适合低频调用的非关键路径(如初始化日志、异常处理)。
  1. 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)”
}

  1. 适用场景
  • Logger:
    • 生产环境的核心服务(如 API 网关、数据库操作)。
    • 需要精确控制日志格式和性能的场景。
    • 日志会被 ELK、Prometheus 等系统收集分析(结构化数据更易处理)。
  • SugaredLogger:
    • 开发阶段的快速调试(如打印临时变量)。
    • 日志格式灵活性要求高的场景(如输出复杂对象)。
    • 非关键路径的低频日志(如配置加载、启动信息)。

三、最佳实践
混合使用:

  • 在性能敏感的代码中使用 Logger,在调试或非关键路径使用 SugaredLogger。
  • 避免在循环中使用 SugaredLogger:
    反射开销在高频调用时会显著影响性能。
  • 生产环境优先使用 Logger:
    结构化日志更易于自动化分析和监控告警。
http://www.dtcms.com/a/279564.html

相关文章:

  • 5.浏览本地文件获取路径与文件名称 C#例子 WPF例子
  • Elasticsearch 9.x 升级变化
  • 【安卓笔记】线程基本使用:锁、锁案例
  • Windows安装postgreSQL(保姆级教程)
  • 机床自动化中的“方言翻译官”:EtherNet/IP 转 PROFIBUS DP 实战手记
  • 安全初级(一)
  • 胡志明证券交易所新一代交易系统解决方案——基于美联储利率决议背景下的越南跨境金融基础设施升
  • pycharm恢复出厂设置,可以解决大多数pycharm存在的问题
  • nginx:SSL_CTX_use_PrivateKey failed
  • 怎么 将训练后的词嵌入向量反编译为自然语言
  • AI多因子模型解析白银14年新高:流动性压力与工业避险需求的联动效应
  • 数字化工厂规划-项目启动会汇报材料编写思路
  • Android Studio C++/JNI/Kotlin 示例 二
  • 三相新能源并网系统序阻抗模型——序阻抗分析器IMAnalyzer
  • Docker部署语音转文字(STT)服务并接入Home Assistant
  • linux服务器redis配置开机自启
  • 2025 R3CTF
  • 我的开源项目-AI Agent 配置系统
  • 技嘉UEFI固件SMM漏洞使系统面临固件植入和持久控制风险
  • Oracle 学习笔记
  • 【工具变量】A股上市公司产学研合作及专利数据统计(1998-2023年)
  • TextIn:文档全能助手,让学习效率飙升的良心软件~
  • 《汇编语言:基于X86处理器》第7章 复习题和练习,编程练习
  • RAG索引流程中的文档解析:工业级实践方案与最佳实践
  • SMTPman,发送邮件服务器smtp怎么填才行?
  • 鹧鸪云:别墅光储项目方案设计的最终选择
  • 面试150 二叉树中的最大路径和
  • 水务工程中自动化应用:EtherNet/IP转PROFIBUS DP连接超声波流量计
  • 9.服务容错:构建高可用微服务的核心防御
  • Go泛型完全指南:从基础到实战应用