Gin 框架学习实录 · 第7篇:日志模块封装

455 阅读5分钟

前言

在完成了用户模块 CRUD、统一响应结构与错误码、分页封装之后,我们已经搭建出一个较为完整的 Gin 项目基础骨架。

但要想让项目在生产环境中更加可观测、易排查、可追踪,日志模块就必不可少了。

日志模块封装(使用 zap 实现)

我们使用 Uber 出品的高性能日志库 zap 来封装日志模块。

步骤一:安装 zap 日志库

go get go.uber.org/zap

步骤二:创建日志模块

logger/logger.go 中初始化日志模块:

// logger/logger.go
package logger

import (
	"go.uber.org/zap"
)

var Log *zap.SugaredLogger

func InitLogger() {
	// 创建生产环境的日志配置(带时间戳、调用行号等)
	zapLogger, err := zap.NewProduction()
	if err != nil {
		panic("❌ 初始化日志失败: " + err.Error())
	}

	// 使用 SugaredLogger 封装,支持格式化输出
	Log = zapLogger.Sugar()
}

步骤三:在 main.go 中初始化日志

// main.go
import (
	"gin-learn-notes/logger"
)

func main() {
	logger.InitLogger()
	defer logger.Log.Sync() // 确保缓冲区日志都被写出

	// 其他初始化...
}

这里我要说一下 我们在上面的main.go里面加入了这行 defer logger.Log.Sync()。 这个我刚开始也不了解 后面才知道:

zap 的日志输出是异步 + 缓冲的

默认情况下,zap 会将日志内容写入内存缓冲区,然后异步地刷新到标准输出或文件中,这样做可以提升日志性能,但也带来一个问题

如果程序还没来得及把日志写入目标输出,主程序就退出了,那些缓冲中的日志就可能“丢失

所以我们要在程序退出前,主动调用 .Sync() 方法

这个方法的作用就是:

  • 刷新 zap 的缓冲区
  • 确保所有日志都已经被写入(终端 or 文件)

步骤四:项目中任意位置调用日志

import "gin-learn-notes/logger"

logger.Log.Info("用户列表:", users)

✅ 使用 zap.SugaredLogger,可以像 Printf 一样使用格式化日志,同时性能也非常优秀。后续也可以替换为自定义配置、输出到文件、JSON 格式等。

我们就可以在控制台看到日志输出:

image.png


配置化日志 + 写入文件 + 按天切割

一般我们日志都会存放到专门的日志目录里面 不会像这样直接输出到控制台中 所以我们可以把日志写入日志文件,并自动切割。

实现方案:zap + lumberjack 搭配使用

lumberjack 是一个非常轻量、稳定的日志切割工具,支持:

  • 最大文件大小
  • 最多保留几个日志文件
  • 是否压缩历史日志
  • 按天/按大小切割等

安装 lumberjack

go get github.com/natefinch/lumberjack

我们可以在 logger.go 里加如下功能:

import (
	"go.uber.org/zap/zapcore"
	"github.com/natefinch/lumberjack"
)

func getLogWriter() zapcore.WriteSyncer {
	lumberJackLogger := &lumberjack.Logger{
		Filename:   "logs/app.log",
		MaxSize:    10,
		MaxBackups: 5,
		MaxAge:     30,
		Compress:   true,
	}
	return zapcore.AddSync(lumberJackLogger)
}

然后我们修改下之前的InitLogger函数:

func InitLogger() {
    // 设置日志输出格式为 JSON
    encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())

    // 设置输出位置(文件写入器)
    writer := getLogWriter()

    // 设置日志等级
    level := zapcore.InfoLevel

    // 创建 core
    core := zapcore.NewCore(encoder, writer, level)

    // 创建 logger
    logger := zap.New(core, zap.AddCaller())
    Log = logger.Sugar()
}

完整文件示例(logger/logger.go):

package logger

import (
    "github.com/natefinch/lumberjack"
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
)

var Log *zap.SugaredLogger

func InitLogger() {
    // 设置日志输出格式为 JSON
    encoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())

    // 设置输出位置(文件写入器)
    writer := getLogWriter()

    // 设置日志等级
    level := zapcore.InfoLevel

    // 创建 core
    core := zapcore.NewCore(encoder, writer, level)

    // 创建 logger
    logger := zap.New(core, zap.AddCaller())
    Log = logger.Sugar()
}

func getLogWriter() zapcore.WriteSyncer {
    lumberJackLogger := &lumberjack.Logger{
       Filename:   "logs/app.log",
       MaxSize:    10,
       MaxBackups: 5,
       MaxAge:     30,
       Compress:   true,
    }
    return zapcore.AddSync(lumberJackLogger)
}

我们需要在项目根目录提前创建 logs/ 目录:

mkdir logs

否则第一次运行时 zap 会找不到路径报错。当然我们也可以创建日志目录判断(避免首次运行时报错):

func getLogWriter() zapcore.WriteSyncer {
    // 自动创建 logs 目录(如果不存在)
    if _, err := os.Stat("logs"); os.IsNotExist(err) {
       _ = os.Mkdir("logs", 0755)
    }

    lumberJackLogger := &lumberjack.Logger{
       Filename:   "logs/app.log",
       MaxSize:    10,
       MaxBackups: 5,
       MaxAge:     30,
       Compress:   true,
    }
    return zapcore.AddSync(lumberJackLogger)
}

我们在此运行一下之前写日志的地方接口后看看是否记录到logs/app.log里面:

image.png

项目结构继续演进(新增日志模块)

gin-learn-notes/
├── config/
│   └── database.go                 // 数据库配置
│
├── controller/
│   ├── hello.go
│   ├── index.go
│   └── user.go                    // 用户接口
│
├── core/
│   └── response/
│       ├── code.go                // 错误码常量
│       ├── page.go                // 分页结构体
│       └── response.go            // 统一响应结构
│
├── logger/
│   └── logger.go                  // ✅ 新增:zap + lumberjack 日志封装
│
├── logs/
│   └── app.log                    // ✅ 自动生成日志文件(建议忽略提交)
│
├── model/
│   └── user.go                    // 用户模型
│
├── request/
│   ├── page_request.go           // 通用分页请求结构体
│   └── user_request.go           // 用户请求参数结构体
│
├── router/
│   └── router.go                 // 路由注册
│
├── service/
│   └── user_service.go           // 用户业务逻辑
│
├── utils/
│   ├── paginate.go               // 通用分页逻辑
│   ├── response.go               // 旧响应结构(逐步迁移)
│   └── validator.go              // 参数校验翻译
│
├── main.go                       // 入口文件,初始化日志等
├── go.mod
├── .gitignore
└── README.md

最后

本篇我们完成了日志模块的初步封装,包括:

  • 使用 zap 构建高性能日志组件
  • 搭配 lumberjack 实现日志自动切割
  • 日志输出到 logs/ 目录,便于运维分析
  • 可在项目中任意位置使用统一日志输出格式

到这为止,我们的 Gin 项目已经具备了清晰的日志体系,可以支撑后续开发与调试。

但是呢 我们注意到:目前数据库连接信息、端口号、日志路径等,还是直接写在代码里的

dsn := "root:123456@tcp(127.0.0.1:3306)/demo?..."
// 日志写入 logs/app.log(写死的路径)

但在实际开发中,这些配置一般都会写在 .envconfig.yaml 等配置文件中,而不是硬编码到逻辑里

所以 接下来我们将使用 config.yaml 实现全局配置管理:

  • 🛠️ 管理数据库连接信息
  • ⚙️ 设置服务端口
  • 📝 配置日志路径、日志等级
  • ✅ 封装成统一的 config 模块,项目启动时自动加载

实现更灵活的配置方式,便于后续环境切换、配置热更新、部署管理等场景

本篇对应代码提交记录

commit: 3cafdb4018d626705c6673f3299a16a98db0f1e6

👉 GitHub 源码地址:github.com/luokakale-k…