GVM 脚手架探险记03:跟着 GVM 学习 Zap
Zap 基本介绍
Zap 是 Go 语言生态中性能顶尖的日志库,由全球知名科技公司 Uber 精心打造。它就像日志世界的「闪电侠」,以无与伦比的速度和 efficiency 著称,让你的应用日志记录既快速又可靠。
想象一下,当你的应用程序在高并发环境下运行,每秒需要处理成千上万条日志时,一个缓慢的日志库可能成为性能瓶颈。Zap 采用了零分配设计和优化的缓冲区管理,能在不牺牲性能的前提下,提供丰富的日志功能。
它的核心特性包括:
- 极致性能:采用零内存分配设计,速度远超其他日志库
- 结构化日志:支持 JSON 格式输出,方便日志分析和检索
- 可配置性:提供丰富的配置选项,满足不同场景需求
- 分级日志:支持 Debug、Info、Warn、Error 等多种日志级别
- 低内存占用:优化的内存管理,减少资源消耗
在 GVM 脚手架中,Zap 被用来处理各种日志记录需求,从开发调试信息到生产环境的错误日志,都能通过它高效地处理。
日志级别说明
常见的日志级别有 Debug 、 Info 、 Warn 、 Error 等,它们的优先级依次升高。
-
Debug 级别 :最低的日志级别,用于开发和调试阶段,会记录大量详细的信息。
设置为 Debug 级别时, Debug 、 Info 、 Warn 、 Error 级别的日志消息都会被记录和输出。
-
Info 级别 :用于记录程序运行过程中的关键信息,帮助开发者了解程序的运行状态。
设置为 Info 级别时, Info 、 Warn 、 Error 级别的日志消息会被记录,而 Debug 级别的日志会被忽略。
-
Warn 级别 :用于记录潜在的问题或异常情况,但程序仍然可以继续运行。
设置为 Warn 级别时,只有 Warn 和 Error 级别的日志消息会被记录, Debug 和 Info 级别的日志会被忽略。
-
Error 级别 :用于记录程序运行过程中发生的错误,这些错误可能会导致程序功能异常或崩溃。
设置为 Error 级别时,只有 Error 级别的日志消息会被记录。
GVM 中 Zap 配置
GVM 采用模块化设计,每个模块库都拥有独立的配置结构体,这些结构体最终会组合到总的应用配置结构体中。更巧妙的是,结构体设计与 Viper 管理的配置文件信息一一对应,实现了配置的无缝衔接。
// config/zap.go
package config
import (
"time"
"go.uber.org/zap/zapcore"
)
type Zap struct {
// 级别
Level string `mapstructure:"level" json:"level" yaml:"level"`
// 日志前缀
Prefix string `mapstructure:"prefix" json:"prefix" yaml:"prefix"`
// 输出
Format string `mapstructure:"format" json:"format" yaml:"format"`
// 日志文件夹
Director string `mapstructure:"director" json:"director" yaml:"director"`
// 编码级
EncodeLevel string `mapstructure:"encode-level" json:"encode-level" yaml:"encode-level"`
// 栈名
StacktraceKey string `mapstructure:"stacktrace-key" json:"stacktrace-key" yaml:"stacktrace-key"`
// 显示行
ShowLine bool `mapstructure:"show-line" json:"show-line" yaml:"show-line"`
// 输出控制台
LogInConsole bool `mapstructure:"log-in-console" json:"log-in-console" yaml:"log-in-console"`
// 日志保留天数
RetentionDay int `mapstructure:"retention-day" json:"retention-day" yaml:"retention-day"`
}
// Levels 根据字符串转化为 zapcore.Levels 切片
// 返回的切片包含从配置级别到 FatalLevel 的所有级别
func (c *Zap) Levels() []zapcore.Level {
// Zap 的日志级别 (zapcore.Level) 是固定的、有限的枚举值,不需要动态扩容
levels := make([]zapcore.Level, 0, 7)
level, err := zapcore.ParseLevel(c.Level)
if err != nil {
level = zapcore.DebugLevel
}
for ; level <= zapcore.FatalLevel; level++ {
levels = append(levels, level)
}
return levels
}
// Encoder 自定义日志编码器
func (c *Zap) Encoder() zapcore.Encoder {
config := zapcore.EncoderConfig{
TimeKey: "time", // 日志内容显示时间的 key 为 time
NameKey: "name", // 日志内容显示名字的 key 为 name
LevelKey: "level", // 日志内容显示日志级别的 key 为 level
CallerKey: "caller", // 日志内容显示调用者的 key 为 caller
MessageKey: "message", // 日志内容显示日志内容的 key 为 message
StacktraceKey: c.StacktraceKey, // 日志内容显示栈名的 key
LineEnding: zapcore.DefaultLineEnding, // 行结束的标志
// 设置时间编码格式为 2006-01-02 15:04:05.000
EncodeTime: func(t time.Time, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString(c.Prefix + t.Format("2006-01-02 15:04:05.000"))
},
// 设置日志级别编码为形式
EncodeLevel: c.LevelEncoder(),
// 将调用日志记录函数的文件和行号编码为短格式
// 例如 /full/path/to/package/file:line
EncodeCaller: zapcore.FullCallerEncoder,
// 将日志中记录的持续时间编码为秒数
EncodeDuration: zapcore.SecondsDurationEncoder,
}
if c.Format == "json" {
// 以 json 格式记录
return zapcore.NewJSONEncoder(config)
}
// 以 console 格式记录
return zapcore.NewConsoleEncoder(config)
}
// LevelEncoder 根据 EncodeLevel 返回 zapcore.LevelEncoder
// 控制日志中日志级别显示样式
func (c *Zap) LevelEncoder() zapcore.LevelEncoder {
switch {
// 小写编码器(默认)
case c.EncodeLevel == "LowercaseLevelEncoder":
return zapcore.LowercaseLevelEncoder
// 小写编码器带颜色
case c.EncodeLevel == "LowercaseColorLevelEncoder":
return zapcore.LowercaseColorLevelEncoder
// 大写编码器
case c.EncodeLevel == "CapitalLevelEncoder":
return zapcore.CapitalLevelEncoder
// 大写编码器带颜色
case c.EncodeLevel == "CapitalColorLevelEncoder":
return zapcore.CapitalColorLevelEncoder
default:
return zapcore.LowercaseLevelEncoder
}
}
Viper 依赖 mapstructure tag 解析配置,与 yaml tag 无关。
在 GVM 所有配置结构体中都拥有 yaml tag 标签,实际测试删除不会对项目运行产生影响。
推测主要为了代码可读性和兼容性,在开发自己项目时可以无需带此 tag。
json 格式记录样式参考
{"level":"\u001b[34minfo\u001b[0m","time":"[github.com/flipped-aurora/gin-vue-admin/server]2025-06-25 14:00:55.403","caller":"E:/Users/wanglei/code/gin-vue-admin/server/initialize/router.go:71","message":"register swagger handler"}
console 格式记录样式参考
[github.com/flipped-aurora/gin-vue-admin/server]2025-06-25 09:41:58.327 [34minfo[0m E:/Users/wanglei/code/gin-vue-admin/server/initialize/router.go:71 register swagger handler
GVM 中 Zap 用法解析
首先从 GVM 中 Zap 初始化的入口文件看
// core/zap.go
package core
import (
"fmt"
"os"
"github.com/flipped-aurora/gin-vue-admin/server/core/internal"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// Zap 获取 zap.Logger
// Author [SliverHorn](https://github.com/SliverHorn)
func Zap() (logger *zap.Logger) {
// 判断是否有Director文件夹
if ok, _ := utils.PathExists(global.GVA_CONFIG.Zap.Director); !ok {
fmt.Printf("create %v directory\n", global.GVA_CONFIG.Zap.Director)
_ = os.Mkdir(global.GVA_CONFIG.Zap.Director, os.ModePerm)
}
// 获取配置日志级别 从配置的级别开始到最高级
levels := global.GVA_CONFIG.Zap.Levels()
length := len(levels)
cores := make([]zapcore.Core, 0, length)
for i := 0; i < length; i++ {
// 构造内核
core := internal.NewZapCore(levels[i])
cores = append(cores, core)
}
logger = zap.New(zapcore.NewTee(cores...))
if global.GVA_CONFIG.Zap.ShowLine {
// 添加调用者信息
logger = logger.WithOptions(zap.AddCaller())
}
return logger
}
这段代码完成了什么功能?
- 首先检查配置中日志输出目录是否存在,不存在则创建并赋予077权限
- 然后根据配置的日志级别,从配置的级别开始到最高级,构造内核
- 最后使用
zap.NewTee
方法将多个内核合并为一个 Tee 内核 - 如果配置中开启了显示调用者信息,则使用
logger.WithOptions
方法添加调用者信息 - 最后返回构造好的
zap.Logger
实例
可以看到,初始化入口方法的实现非常简洁明了,核心代码逻辑都封装在 internal.NewZapCore
方法中。
// core/internal/zap_core.go
type ZapCore struct {
level zapcore.Level
zapcore.Core
}
func NewZapCore(level zapcore.Level) *ZapCore {
entity := &ZapCore{level: level}
// 日志显示(写入)到的位置
syncer := entity.WriteSyncer()
levelEnabler := zap.LevelEnablerFunc(func(l zapcore.Level) bool {
return l == level
})
entity.Core = zapcore.NewCore(global.GVA_CONFIG.Zap.Encoder(), syncer, levelEnabler)
return entity
}
从代码中可以看到,GVM 自定义了一个 ZapCore
结构体。该结构体包含两个关键部分:一是自定义的日志级别阈值字段 level
,二是内嵌的 Zap 核心接口 Core
。最终,通过调用 zapcore.NewCore
方法,成功构造了一个实现了 Core
接口的 zapcore.ioCore
实例。
zapcore.NewCore
方法接收的三个参数的作用分别是:
- encoder : 决定日志的格式化方式(如 JSON/文本、时间格式、字段排列等)
- syncer : 指定日志写入的目标(如文件、控制台、网络等)
- levelEnabler : 动态过滤日志级别(只有符合条件的日志会被记录)
syncer := entity.WriteSyncer()
方法暂时放到后面文章讲解,涉及到了文件等操作
继续往下继续看代码,我们会发现 ZapCore
结构体除了方法 WriteSyncer
外,还有很多的其余方法,这些方法又是做什么的,为什么要定义它们呢?
cores := make([]zapcore.Core, 0, length)
for i := 0; i < length; i++ {
// 构造内核
core := internal.NewZapCore(levels[i])
cores = append(cores, core)
}
logger = zap.New(zapcore.NewTee(cores...))
在入口文件中已经得到了不同日志级别的 Zap 日志内核,存放在了一个切片中,最后会通过 zap.NewTee
方法将多个内核合并为一个 Tee 内核。
zapcore.NewTee()
的方法定义如下:
// NewTee creates a Core that duplicates log entries into two or more
// underlying Cores.
//
// Calling it with a single Core returns the input unchanged, and calling
// it with no input returns a no-op Core.
func NewTee(cores ...Core) Core {
switch len(cores) {
case 0:
return NewNopCore()
case 1:
return cores[0]
default:
return multiCore(cores)
}
}
可以看到,zapcore.NewTee()
方法接收一个 Core
接口类型的切片参数,返回一个合并后的 Core
实例。
在 Golang 中只要实现了接口定义的所有方法,那么就可以认为它实现了这个接口。
因此 GVM 的 ZapCore
还需要额外的那么多方法。
具体方法如下:
// 因为是自定义的 zapcore.Core 结构体,为了
// logger = zap.New(zapcore.NewTee(cores...))
// 使用 需要实现 接口的方法
//zapcore.NewCore 自带这些方法
// Enabled 实现 type LevelEnabler interface
func (z *ZapCore) Enabled(level zapcore.Level) bool {
return z.level == level
}
// With 调用的 zapcore.NewCore With 方法
func (z *ZapCore) With(fields []zapcore.Field) zapcore.Core {
return z.Core.With(fields)
}
// Check 参考 zapcore.NewCore Check 方法
func (z *ZapCore) Check(entry zapcore.Entry, check *zapcore.CheckedEntry) *zapcore.CheckedEntry {
if z.Enabled(entry.Level) {
return check.AddCore(entry, z)
}
return check
}
// Write
func (z *ZapCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
// 如果日志中包含 business、folder 或 directory 字段
//则根据字段值动态创建新的输出目标(如单独的文件目录),并永久切换至此目标
for i := 0; i < len(fields); i++ {
if fields[i].Key == "business" || fields[i].Key == "folder" || fields[i].Key == "directory" {
// 根据字段值动态创建新的输出目标(syncer)
syncer := z.WriteSyncer(fields[i].String)
// 永久替换 Core
z.Core = zapcore.NewCore(global.GVA_CONFIG.Zap.Encoder(), syncer, z.level)
}
}
// 调用的 zapcore.NewCore Write 方法
return z.Core.Write(entry, fields)
}
// Sync 调用的 zapcore.NewCore Sync 方法
func (z *ZapCore) Sync() error {
return z.Core.Sync()
}
这里很多的方法不需要理解,它本身也参考了实现了 zapcore.Core
接口的 zapcore.ioCore
结构体方法。
实际上底层还是调用的是 zapcore.ioCore
结构体的方法。ZapCore
方法是对 zapcore.ioCore
方法的封装,添加了一些 GVM 自己的逻辑。
简单使用 Zap
一般项目推荐用法
这里采用日志轮转 lumberjack 库作为 Zap 的日志写入的目标,和 GVM 项目相比较日志等级没有区分的特别细,日志都记录在一个文件中。
但具备基本的日志记录功能,可以设置最大的日志文件大小和日志文件数量。搭配一般的日志查询使用也足够日常使用。
package logger
import (
"os"
"gitee.com/iswleii/wechat-weather/internal/config"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func InitLogger(cfg config.LogConfig, mode string) (*zap.Logger, error) {
// 日志轮转配置
writeSyncer := cfg.GetLogWriter()
// 尝试将配置文件里定义的日志级别字符串 解析成 zapcore.Level 类型的值
l, err := zapcore.ParseLevel(cfg.Level)
if err != nil {
return nil, err
}
// 自定义日志编码器
encoder := cfg.GetEncoder()
var core zapcore.Core
// 当是开发模式,将日志信息打印输出到控制台一份
if mode == "dev" {
consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
core = zapcore.NewTee(
zapcore.NewCore(consoleEncoder, zapcore.Lock(os.Stdout), zapcore.DebugLevel),
zapcore.NewCore(encoder, writeSyncer, l),
)
} else {
core = zapcore.NewCore(encoder, writeSyncer, l)
}
lg := zap.New(core, zap.AddCaller())
// 替换zap包中全局的logger实例,后续在其他包中只需使用zap.L()调用即可
zap.ReplaceGlobals(lg)
return lg, nil
}
超级基础用法
将日志写入到指定文件中,其余配置均采用 Zap 预设的默认值。
func InitLogger() {
writeSyncer := getLogWriter()
encoder := getEncoder()
core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel)
logger := zap.New(core)
sugarLogger = logger.Sugar()
}
func getEncoder() zapcore.Encoder {
return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
}
func getLogWriter() zapcore.WriteSyncer {
file, _ := os.Create("./test.log")
return zapcore.AddSync(file)
}