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.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中使用的選擇區號的組件,包含不同國家國旗圖標。 效果圖 別的不說,先來…