三十的博客

GVM 脚手架探险记03:跟着 GVM 学习 Zap

发布时间
阅读量 加载中...

Zap 基本介绍

Zap 是 Go 语言生态中性能顶尖的日志库,由全球知名科技公司 Uber 精心打造。它就像日志世界的「闪电侠」,以无与伦比的速度和 efficiency 著称,让你的应用日志记录既快速又可靠。

想象一下,当你的应用程序在高并发环境下运行,每秒需要处理成千上万条日志时,一个缓慢的日志库可能成为性能瓶颈。Zap 采用了零分配设计和优化的缓冲区管理,能在不牺牲性能的前提下,提供丰富的日志功能。

它的核心特性包括:

在 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 管理的配置文件信息一一对应,实现了配置的无缝衔接。

go
// 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 格式记录样式参考

go
{"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 格式记录样式参考

go
[github.com/flipped-aurora/gin-vue-admin/server]2025-06-25 09:41:58.327	info	E:/Users/wanglei/code/gin-vue-admin/server/initialize/router.go:71	register swagger handler

GVM 中 Zap 用法解析

首先从 GVM 中 Zap 初始化的入口文件看

go
// 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
}

这段代码完成了什么功能?

  1. 首先检查配置中日志输出目录是否存在,不存在则创建并赋予077权限
  2. 然后根据配置的日志级别,从配置的级别开始到最高级,构造内核
  3. 最后使用 zap.NewTee 方法将多个内核合并为一个 Tee 内核
  4. 如果配置中开启了显示调用者信息,则使用 logger.WithOptions 方法添加调用者信息
  5. 最后返回构造好的 zap.Logger 实例

可以看到,初始化入口方法的实现非常简洁明了,核心代码逻辑都封装在 internal.NewZapCore 方法中。

go
// 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 方法接收的三个参数的作用分别是:

syncer := entity.WriteSyncer() 方法暂时放到后面文章讲解,涉及到了文件等操作

继续往下继续看代码,我们会发现 ZapCore 结构体除了方法 WriteSyncer 外,还有很多的其余方法,这些方法又是做什么的,为什么要定义它们呢?

go
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() 的方法定义如下:

go
// 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 还需要额外的那么多方法。

具体方法如下:

go
// 因为是自定义的 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 项目相比较日志等级没有区分的特别细,日志都记录在一个文件中。

但具备基本的日志记录功能,可以设置最大的日志文件大小和日志文件数量。搭配一般的日志查询使用也足够日常使用。

go
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 预设的默认值。

go
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)
}
#Gvm #Golang