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.pswp.cn/bicheng/89190.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/89190.shtml
英文地址,请注明出处:http://en.pswp.cn/bicheng/89190.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

UI前端大数据处理新挑战:如何高效处理实时数据流?

hello宝子们...我们是艾斯视觉擅长ui设计和前端数字孪生、大数据、三维建模、三维动画10年经验!希望我的分享能帮助到您!如需帮助可以评论关注私信我们一起探讨!致敬感谢感恩!一、引言:从 “批处理” 到 “流处理” 的前端革命当股票 APP 因每秒接收 10 万条行情数据…

【接口测试】08 Postman使用教程(带案例)

目录 一. Postman安装 二. Postman使用 1. 创建项目 2. 创建集合 3. 设置变量 4. 创建测试用例 5. 数据驱动测试 6. 接口关联 7. 断言和封装 8. 批量执行 9. 导出用例 10. 生成测试报告 一. Postman安装 PostMan——安装教程(图文详解)_postman安装教程-…

从springcloud-gateway了解同步和异步,webflux webMvc、共享变量

webMVC和webFlux 这是spring framework提供的两种不同的Web编程模型应用场景:用 WebMvc: 项目依赖 Servlet 生态、需要简单同步代码,或使用阻塞式数据库(如 MySQL JDBC)。用 WebFlux: 需要高并发&#xff…

如何在 Pytest 中调用其他用例返回的接口参数?

回答重点在 Pytest 中,我们可以通过使用共享夹具(fixtures)来调用和复用其他用例返回的接口参数。在 Pytest 中,fixtures 提供了一种灵活且有组织的方式来共享测试数据或对象。具体步骤如下:1)首先&#xf…

倒计时熔断机制的出价逻辑

一、业务背景传统竞价机制中,“倒计时结束”是系统决定成交者的关键逻辑,但在实际中,最后3秒突然被抢价的情况极为常见,出现以下问题:用户投诉平台机制不公平;用户出价但未成交,产生争议订单&am…

未来手机会自动充电吗

未来手机实现‌全自动充电(无需人为干预)‌是技术发展的明确趋势,目前已有部分技术落地,但要达到“随时随地无感补电”,仍需突破以下关键领域:一、已实现的技术(当下可用的“半自动”充电&#…

MySQL高级篇(二):深入理解数据库事务与MySQL锁机制

引言在现代数据库系统中,事务和锁机制是确保数据一致性和完整性的两大核心技术。无论是金融交易系统、电商平台还是企业级应用,都离不开这些基础功能的支持。本文将全面剖析数据库事务的四大特性,深入探讨MySQL中的各种锁机制,帮助…

XML 指南

XML 指南 引言 XML(可扩展标记语言)是一种用于存储和传输数据的标记语言,它具有高度的可扩展性和灵活性。在互联网和软件开发领域,XML被广泛应用于数据交换、配置文件、文档存储等场景。本文将为您详细介绍XML的基本概念、语法规则、应用场景以及开发技巧,帮助您全面了解…

Flink Watermark原理与实战

一、引言Flink 作为一款强大的流处理框架,在其中扮演着关键角色。今天,咱们来聊聊 Flink 中一个极为重要的概念 —— Watermark(水位线),它是处理乱序数据和准确计算的关键。接下来我们直入主题,首先来看看…

Rust Web 全栈开发(五):使用 sqlx 连接 MySQL 数据库

Rust Web 全栈开发(五):使用 sqlx 连接 MySQL 数据库Rust Web 全栈开发(五):使用 sqlx 连接 MySQL 数据库项目创建数据库准备连接请求功能实现Rust Web 全栈开发(五):使用…

【zynq7020】PS的“Hello World”

目录 基本过程 新建Vivado工程 ZYNQ IP核设置 使用SDK进行软件开发 基于Vivado2017 Vivado工程建立 SDK调试 固化程序 注:Vivado 2019.1 及之前:默认使用 SDK Vivado 2019.2-2020.1:逐步过渡,支持 SDK 与 Vitis 并存 Vi…

希尔排序和选择排序及计数排序的简单介绍

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数gap,把待排序文件中所有数据分成几个组,所有距离为gap的数据分在同一组内,并对每一组内的数据进行排序。然后gap减减,重复上述分组和排序的工作。当到…

Solid Edge多项目并行,浮动许可如何高效调度?

在制造企业的数字化设计体系中,Solid Edge 作为主流 CAD 工具,因其灵活的建模能力、同步技术和强大的装配设计功能,广泛应用于机械设备、零部件制造等行业的研发场景。随着企业设计任务复杂化,多项目并行成为常态,Soli…

Flink cdc 使用总结

Flink 与 Flink CDC 版本兼容对照表Flink 版本支持的 Flink CDC 版本关键说明Flink 1.11.xFlink CDC 1.2.x早期版本,需注意 Flink 1.11.0 的 Bug(如 Upsert 写入问题),建议使用 1.11.1 及以上。Flink 1.12.xFlink CDC 2.0.x&#…

企业培训笔记:axios 发送 ajax 请求

文章目录axios 简介一,Vue工程中安装axios二,编写app.vue三,编写HomeView.vue四,Idea打开后台项目五,创建HelloController六,配置web访问端口七,运行项目,查看效果(一&am…

Maven下载与配置对Java项目的理解

目录 一、背景 二、JAVA项目与Maven的关系 2.1标准java项目 2.2 maven 2.2.1 下载maven 1、下载 2、配置环境 2.2.2 setting.xml 1、配置settings.xml 2、IDEA配置maven 一、背景 在java项目中,新手小白很有可能看不懂整体的目录结构,以及每个…

Mars3d的走廊只能在一个平面的无法折叠的解决方案

问题场景:1. Mars3d的CorridorEntity只能在一个平面修改高度值,无法根据坐标点位制作有高度值的走廊效果,想要做大蜀山盘山走廊的效果实现不了。解决方案:1.使用原生cesium实现对应的走廊的截面形状、走廊的坐标点,包括…

LeetCode 每日一题 2025/7/7-2025/7/13

记录了初步解题思路 以及本地实现代码;并不一定为最优 也希望大家能一起探讨 一起进步 目录7/7 1353. 最多可以参加的会议数目7/8 1751. 最多可以参加的会议数目 II7/9 3439. 重新安排会议得到最多空余时间 I7/10 3440. 重新安排会议得到最多空余时间 II7/11 3169. …

Bash常见条件语句和循环语句

以下是 Bash 中常用的条件语句和循环语句分类及语法说明,附带典型用例:一、条件语句 1. if 语句 作用:根据条件执行不同代码块 语法: if [ 条件 ]; then# 条件为真时执行 elif [ 其他条件 ]; then# 其他条件为真时执行 else# 所有…

uni-app 选择国家区号

uni-app选择国家区号组件 hy-countryPicker 我们在做登录注册功能的时候,可能会遇到选择区号来使用不同国家手机号来登录或者注册的功能。这里我就介绍下我这个uni-app中使用的选择区号的组件,包含不同国家国旗图标。 效果图 别的不说,先来…