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

发布于:2025-07-15 ⋅ 阅读:(22) ⋅ 点赞:(0)

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

统一日志输出



前言

在 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 logger

import (
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"gopkg.in/natefinch/lumberjack.v2"
	"os"
	"time"
)

var Logger *zap.Logger
var Sugar *zap.SugaredLogger

func 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.InfoLevel
	if 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,   // 每个日志文件最大 10MB
		MaxBackups: 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,
		),
	)
	
	// 创建 Logger
	Logger = 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 app

import (
	"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())
	// 配置CORS
	router.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:
    结构化日志更易于自动化分析和监控告警。

网站公告

今日签到

点亮在社区的每一天
去签到