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:
結構化日志更易于自動化分析和監控告警。