摘抄自:https://www.cnblogs.com/jiujuan/p/17304844.html
一、介绍
zap的特性
高性能:zap 对日志输出进行了多项优化以提高它的性能
日志分级:有 Debug,Info,Warn,Error,DPanic,Panic,Fatal 等
日志记录结构化:日志内容记录是结构化的,比如 json 格式输出
自定义格式:用户可以自定义输出的日志格式
自定义公共字段:用户可以自定义公共字段,大家输出的日志内容就共同拥有了这些字段
调试:可以打印文件名、函数名、行号、日志时间等,便于调试程序
自定义调用栈级别:可以根据日志级别输出它的调用栈信息
Namespace:日志命名空间。定义命名空间后,所有日志内容就在这个命名空间下。命名空间相当于一个文件夹
支持 hook 操作
做了哪些优化
基于反射的序列化和字符串格式化,它们都是 CPU 密集型计算且分配很多小的内存。具体到 Go 语言中,使用 encoding/json 和 fmt.Fprintf 格式化 interface{} 会使程序性能降低。
Zap 咋解决呢?Zap 使用一个无反射、零分配的 JSON 编码器,基础 Logger(完全无反射) 尽可能避免序列化开销和内存分配开销。在此基础上,zap 还构建了更高级的 SuggaredLogger(有反射)。
安装
二、日志记录器logger使用
2.1 两种日志记录器
zap 提供了 2 种日志记录器:SugaredLogger
和 Logger
。
在需要性能但不是很重要的情况下,使用 SugaredLogger 较合适。它比其它结构化日志包快 4-10 倍,包括 结构化日志和 printf 风格的 API。看下面使用 SugaredLogger 例子:
1 2 3 4 5 6 7 8 9 10 logger, _ := zap.NewProduction()defer logger.Sync() sugar := logger.Sugar() sugar.Infow("failed to fetch URL" , "url" , url, "attempt" , 3 , "backoff" , time.Second, ) sugar.Infof("Failed to fetch URL: %s" , url)
当性能和类型安全很重要时,请使用 Logger。它比 SugaredLogger 更快,分配的资源更少,但它只支持结构化日志和强类型字段。
1 2 3 4 5 6 7 8 logger, _ := zap.NewProduction()defer logger.Sync() logger.Info("failed to fetch URL" , zap.String("url" , url), zap.Int("attempt" , 3 ), zap.Duration("backoff" , time.Second), )
sugared logger:
它有很好的性能,比一般日志包快 4-10 倍。
支持结构化的日志。
支持 printf 风格的日志。
日志字段不需要定义类型
logger(没有sugar)
它的性能比 sugared logger 还要快 。
它只支持强类型的结构化日志。
它应用在对性能更加敏感日志记录中,它的内存分配次数更少。比如如果每一次内存分配都很重要的话可以使用这个。对类型安全有严格要求也可以使用这个。
logger 和 sugaredlogger 相互转换:
1 2 3 4 5 6 7 8 9 logger := zap.NewExample()defer logger.Sync() sugar := logger.Sugar() plain := sugar.Desugar()
怎么选择:
需要不错的性能但不是很重要的情况下,可以选择 sugaredlogger。它支持结构化日志和 printf 风格的日志记录。sugaredlogger 的日志记录是松散类型的,不是强类型,能接受可变数量的键值对。如果你要用强类型字段记录,可以使用 SugaredLogger.With 方法。
如果是每次或每微秒记录日志都很重要情况下,可以使用 logger,它比 sugaredlogger 每次分配内存更少,性能更高。但它仅支持强类型的结构化日志记录。
2.2 怎么打印结构体
在 Zap 中打印结构体有多种方式,可根据性能需求和开发场景选择合适的方案。以下是详细的实现方法及示例:
一、使用 zap.Reflect
(简单但有反射开销)
zap.Reflect
会通过反射递归序列化结构体,包含所有可导出字段名和值,类似 fmt.Printf("%+v", struct)
的效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package mainimport ( "log" "time" "go.uber.org/zap" "go.uber.org/zap/zapcore" )type User struct { ID string `json:"user_id"` Name string `json:"user_name"` Age int `json:"age"` CreatedAt time.Time `json:"created_at"` }func main () { logger, err := zap.NewProduction() if err != nil { log.Fatalf("failed to create logger: %v" , err) } defer logger.Sync() user := User{ ID: "u123" , Name: "Bob" , Age: 25 , CreatedAt: time.Now(), } logger.Info("用户信息" , zap.Reflect("user" , user)) }
输出:
1 { "level" : "info" , "msg" : "用户信息" , "user" : { "user_id" : "u123" , "user_name" : "Bob" , "age" : 25 , "created_at" : "2025-06-08T12:00:00+08:00" } }
注意事项 :
反射开销 :每次调用 zap.Reflect
都会触发反射,频繁使用会影响性能(尤其在高并发场景)。
字段名 :使用结构体字段的 json
标签(若无则用字段名)。
适用场景 :开发调试、非性能敏感的日志记录。
二、实现 zapcore.ObjectMarshaler
接口(高性能方案)
通过自定义结构体的序列化逻辑,避免反射开销,性能与直接调用 zap.String
等方法相当。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package mainimport ( "log" "time" "go.uber.org/zap" "go.uber.org/zap/zapcore" )type User struct { ID string `json:"user_id"` Name string `json:"user_name"` Age int `json:"age"` CreatedAt time.Time `json:"created_at"` }func (u User) MarshalLogObject(enc zapcore.ObjectEncoder) error { enc.AddString("ID" , u.ID) enc.AddString("Name" , u.Name) enc.AddInt("Age" , u.Age) enc.AddString("CreatedAt" , u.CreatedAt.Format("2006-01-02 15:04:05" )) return nil }func main () { logger, err := zap.NewProduction() if err != nil { log.Fatalf("failed to create logger: %v" , err) } defer logger.Sync() user := User{ ID: "u123" , Name: "Bob" , Age: 25 , CreatedAt: time.Now(), } logger.Info("用户信息" , zap.Object("user" , user)) }
输出结果 :
1 { "level" : "info" , "msg" : "用户信息" , "user" : { "ID" : "u123" , "Name" : "Bob" , "Age" : 25 , "CreatedAt" : "2025-06-08 12:00:00" } }
优点 :
无反射开销 :手动定义序列化逻辑,性能最优。
灵活控制 :可自定义字段名、格式(如时间格式化)。
支持嵌套 :结构体中的嵌套类型也可通过实现接口优化。
适用场景 :生产环境、高性能要求的日志记录。
三、使用 zap.Any
(折中方案)
zap.Any
会根据类型自动选择序列化方式:
若类型实现了 zapcore.ObjectMarshaler
或 json.Marshaler
,则使用接口序列化(无反射)。
否则,退化为 zap.Reflect
(反射序列化)。
1 logger.Info("用户信息" , zap.Any("user" , user))
使用场景 :
当结构体可能实现接口时,优先使用 zap.Any
,避免重复代码。
开发中不确定类型是否需要优化时,作为过渡方案。
四、自定义编码器(高级场景)
若需完全控制序列化(如包含未导出字段、特殊格式),可自定义编码器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package mainimport ( "log" "time" "go.uber.org/zap" "go.uber.org/zap/zapcore" )type User struct { id string name string age int createdAt time.Time }type UserEncoder Userfunc (u UserEncoder) MarshalLogObject(enc zapcore.ObjectEncoder) error { enc.AddString("id" , u.id) enc.AddString("name" , u.name) enc.AddInt("age" , u.age) enc.AddString("created_at" , u.createdAt.Format("2006-01-02" )) return nil }func main () { logger, err := zap.NewProduction() if err != nil { log.Fatalf("failed to create logger: %v" , err) } defer logger.Sync() user := User{ id: "u123" , name: "Bob" , age: 25 , createdAt: time.Now(), } logger.Info("用户信息" , zap.Object("user" , UserEncoder(user))) }
输出结果 :
1 { "level" : "info" , "msg" : "用户信息" , "user" : { "id" : "u123" , "name" : "Bob" , "age" : 25 , "created_at" : "2025-06-08" } }
五、SugaredLogger 中的结构体打印
SugaredLogger(logger.Sugar()
)不直接支持结构体格式化,但可通过 With
方法结合上述方案:
1 2 3 4 sugar := logger.Sugar() sugar.With("user" , zap.Reflect("user" , user)).Info("用户信息" ) sugar.With("user" , user).Info("用户信息" )
性能与场景总结
方法
反射开销
性能(高→低)
适用场景
zap.Object
+ 接口
无
最高
生产环境、高频日志记录
zap.Any
部分有
中
代码简洁性与性能的平衡
zap.Reflect
有
最低
开发调试、低频日志记录
建议 :
生产环境中,对高频日志的结构体实现 zapcore.ObjectMarshaler
接口。
开发调试时,使用 zap.Reflect
快速验证日志格式。
对偶尔记录的结构体,使用 zap.Any
简化代码。
三、logger创建
zap 为我们提供了三种快速创建 logger 的方法: zap.NewProduction()
,zap.NewDevelopment()
,zap.NewExample()
。
见名思义,Example 一般用在测试代码中,Development 用在开发环境中,Production 用在生成环境中。这三种方法都预先设置好了配置信息。
3.1 NewExample()使用
NewExample 构建一个 logger,专门为在 zap 的测试示例使用。它将 DebugLevel 及以上日志用 JSON 格式标准输出,但它省略了时间戳和调用函数,以保持示例输出的简短和确定性。
为什么说 zap.NewExample()
是 zap 为我们提供快速创建 logger 的方法呢?
因为在这个方法里,zap 已经定义好了日志配置项部分默认值。来看它的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 func NewExample (options ...Option) *Logger { encoderCfg := zapcore.EncoderConfig{ MessageKey: "msg" , LevelKey: "level" , NameKey: "logger" , EncodeLevel: zapcore.LowercaseLevelEncoder, EncodeTime: zapcore.ISO8601TimeEncoder, EncodeDuration: zapcore.StringDurationEncoder, } core := zapcore.NewCore(zapcore.NewJSONEncoder(encoderCfg), os.Stdout, DebugLevel) return New(core).WithOptions(options...) }
使用例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport ( "go.uber.org/zap" )func main () { logger := zap.NewExample() logger.Debug("this is debug message" ) logger.Info("this is info message" ) logger.Info("this is info message with fileds" , zap.Int("age" , 37 ), zap.String("agender" , "man" ), ) logger.Warn("this is warn message" ) logger.Error("this is error message" ) }
输出:
1 2 3 4 5 {"level":"debug","msg":"this is debug message"} {"level":"info","msg":"this is info message"} {"level":"info","msg":"this is info message with fileds","age":37,"agender":"man"} {"level":"warn","msg":"this is warn message"} {"level":"error","msg":"this is error message"}
3.2 NewDevelopment()使用
NewDevelopment() 构建一个开发使用的 Logger,它以人性化的格式将 DebugLevel 及以上日志信息输出。它的底层使用
NewDevelopmentConfig().Build(...Option)
构建。它的日志格式各种设置在函数 NewDevelopmentEncoderConfig() 里,想查看详情设置,请点进去查看。
使用例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package mainimport ( "time" "go.uber.org/zap" )func main () { logger, _ := zap.NewDevelopment() defer logger.Sync() logger.Info("failed to fetch url" , zap.String("url" , "http://example.com" ), zap.Int("attempt" , 3 ), zap.Duration("duration" , time.Second), ) logger.With( zap.String("url" , "http://development.com" ), zap.Int("attempt" , 4 ), zap.Duration("duration" , time.Second*5 ), ).Info("[With] failed to fetch url" ) }
输出(下面日志输出了文件名和行号,NewExample() 没有)
1 2 2023-03-22T16:02:45.760+0800 INFO zapdemos/newdevelopment1.go:13 failed to fetch url {"url": "http://example.com", "attempt": 3, "duration": "1s"} 2023-03-22T16:02:45.786+0800 INFO zapdemos/newdevelopment1.go:25 [With] failed to fetch url {"url": "http://development.com", "attempt": 4, "duration": "5s"}
Example 和 Production 是 json 格式输出,Development 是普通一行格式输出,如果后面带有字段输出话用json格式。
3.3 NewProduction()使用
NewProduction() 构建了一个合理的 Prouction 日志记录器,它将 info 及以上的日志内容以 JSON 格式记写入标准错误里。
它的底层使用 NewProductionConfig().Build(...Option)
构建。它的日志格式设置在函数 NewProductionEncoderConfig 里。
使用例子 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package mainimport ( "time" "go.uber.org/zap" )func main () { logger, _ := zap.NewProduction() defer logger.Sync() url := "http://zap.uber.io" sugar := logger.Sugar() sugar.Infow("failed to fetch URL" , "url" , url, "attempt" , 3 , "time" , time.Second, ) sugar.Infof("Failed to fetch URL: %s" , url) }
输出:
1 2 {"level":"info","ts":1679472893.2944522,"caller":"zapdemos/newproduction1.go:16","msg":"failed to fetch URL","url":"http://zap.uber.io","attempt":3,"time":1} {"level":"info","ts":1679472893.294975,"caller":"zapdemos/newproduction1.go:22","msg":"Failed to fetch URL: http://zap.uber.io"}
上面日志输出了文件名和行号,NewExample() 没有
直接使用 NewProduction() 就好了,反正都是要打印到文件中的
3.4 使用配置
在这 3 个函数中,可以传入一些配置项。为什么能传入配置项?我们来看看 NewExample() 函数定义:
1 func NewExample(options ...Option) *Logger
它的函数传参有一个 ...Option
选项,是一个 interface 类型,它关联的是 Logger struct。只要返回 Option 就可以传进 NewExample() 里。在 zap/options.go 文件中可以看到很多返回 Option 的函数,也就是说这些函数都可以传入 NewExample 函数里。这里用到了 Go 里面的一个编码技巧,函数选项模式。
zap.Fields() 添加字段到 Logger 中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport ( "go.uber.org/zap" )func main () { logger, _ := zap.NewProduction(zap.Fields( zap.String("log_name" , "testlog" ), zap.String("log_author" , "prometheus" ), )) defer logger.Sync() logger.Info("test fields output" ) logger.Warn("warn info" ) }
输出:
1 2 {"level":"info","ts":1679477929.842166,"caller":"zapdemos/fields.go:14","msg":"test fields output","log_name":"testlog","log_author":"prometheus"} {"level":"warn","ts":1679477929.842166,"caller":"zapdemos/fields.go:16","msg":"warn info","log_name":"testlog","log_author":"prometheus"}
zap.Hook() 添加回调函数:
Hook (钩子函数)回调函数为用户提供一种简单方法,在每次日志内容记录后运行这个回调函数,执行用户需要的操作。也就是说记录完日志后你还想做其它事情就可以调用这个函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport ( "fmt" "go.uber.org/zap" "go.uber.org/zap/zapcore" )func main () { logger := zap.NewExample(zap.Hooks(func (entry zapcore.Entry) error { fmt.Println("[zap.Hooks]test Hooks" ) return nil })) defer logger.Sync() logger.Info("test output" ) logger.Warn("warn info" ) }
输出:
1 2 3 4 {"level":"info","msg":"test output"} [zap.Hooks]test Hooks {"level":"warn","msg":"warn info"} [zap.Hooks]test Hooks
zap.Namespace() :
创建一个命名空间,后面的字段都在这名字空间中。Namespace 就像一个文件夹,后面文件都放在这个文件夹里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport ( "go.uber.org/zap" )func main () { logger := zap.NewExample() defer logger.Sync() logger.Info("some message" , zap.Namespace("shop" ), zap.String("name" , "LiLei" ), zap.String("grade" , "No2" ), ) logger.Error("some error message" , zap.Namespace("shop" ), zap.String("name" , "LiLei" ), zap.String("grade" , "No3" ), ) }
输出:
1 2 {"level" :"info" ,"msg" :"some message" ,"shop" :{"name" :"LiLei" ,"grade" :"No2" }} {"level" :"error" ,"msg" :"some error message" ,"shop" :{"name" :"LiLei" ,"grade" :"No3" }}
四、自定义配置
快速构建 logger 日志记录器最简单的方法就是用 zap 预定义了配置的方法:NewExample(), NewProduction()
和NewDevelopment()
,这 3 个方法通过单个函数调用就可以构建一个日志计记录器,也可以简单配置。
但是有的项目需要更多的定制,怎么办?zap 的 Config 结构和 zapcore 的 EncoderConfig 结构可以帮助你,让你能够进行自定义配置。
4.1 配置结构说明
Config 配置项源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 type Config struct { Level AtomicLevel `json:"level" yaml:"level"` Development bool `json:"development" yaml:"development"` DisableCaller bool `json:"disableCaller" yaml:"disableCaller"` DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"` Sampling *SamplingConfig `json:"sampling" yaml:"sampling"` Encoding string `json:"encoding" yaml:"encoding"` EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"` OutputPaths []string `json:"outputPaths" yaml:"outputPaths"` ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"` InitialFields map [string ]interface {} `json:"initialFields" yaml:"initialFields"` }
EncoderConfig 结构源码,它里面也有很多配置选项,具体请看 这里 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 type EncoderConfig struct { MessageKey string `json:"messageKey" yaml:"messageKey"` LevelKey string `json:"levelKey" yaml:"levelKey"` TimeKey string `json:"timeKey" yaml:"timeKey"` NameKey string `json:"nameKey" yaml:"nameKey"` CallerKey string `json:"callerKey" yaml:"callerKey"` FunctionKey string `json:"functionKey" yaml:"functionKey"` StacktraceKey string `json:"stacktraceKey" yaml:"stacktraceKey"` SkipLineEnding bool `json:"skipLineEnding" yaml:"skipLineEnding"` LineEnding string `json:"lineEnding" yaml:"lineEnding"` EncodeLevel LevelEncoder `json:"levelEncoder" yaml:"levelEncoder"` EncodeTime TimeEncoder `json:"timeEncoder" yaml:"timeEncoder"` EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"` EncodeCaller CallerEncoder `json:"callerEncoder" yaml:"callerEncoder"` EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"` NewReflectedEncoder func (io.Writer) ReflectedEncoder `json:"-" yaml:"-"` ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"` }type Entry struct { Level Level Time time.Time LoggerName string Message string Caller EntryCaller Stack string }
4.2 例子1:基本配置
zap.Config 自定义配置,看官方的一个基本例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package mainimport ( "encoding/json" "go.uber.org/zap" )func main () { rawJSON := []byte (`{ "level": "debug", "encoding": "json", "outputPaths": ["stdout", "./logs.log"], "errorOutputPaths": ["stderr"], "initialFields": {"foo": "bar"}, "encoderConfig": { "messageKey": "message-customer", "levelKey": "level", "levelEncoder": "lowercase" } }` ) var cfg zap.Config if err := json.Unmarshal(rawJSON, &cfg); err != nil { panic (err) } logger := zap.Must(cfg.Build()) defer logger.Sync() logger.Info("logger construction succeeded" ) }
consol 输出如下:
1 {"level":"info","message-customer":"logger construction succeeded","foo":"bar"}
并且在程序目录下生成了一个文件 logs.log,里面记录的日志内容也是上面consol输出内容。每运行一次就在日志文件末尾append一次内容。
4.3 例子2:高级配置
上面的配置只是基本的自定义配置,如果有一些复杂的需求,比如在多个文件之间分割日志。
或者输出到不是 file 的文件中,比如输出到 kafka 中,那么就需要使用 zapcore 包。
在下面的例子中,我们将把日志输出到 kafka 中,并且也输出到 console 里。并且我们对 kafka 不同主题进行编码设置,对输出到 console 编码进行设置,也希望处理高优先级的日志。
官方例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package mainimport ( "io" "os" "go.uber.org/zap" "go.uber.org/zap/zapcore" )func main () { highPriority := zap.LevelEnablerFunc(func (lvl zapcore.Level) bool { return lvl >= zapcore.ErrorLevel }) lowPriority := zap.LevelEnablerFunc(func (lvl zapcore.Level) bool { return lvl < zapcore.ErrorLevel }) topicDebugging := zapcore.AddSync(io.Discard) topicErrors := zapcore.AddSync(io.Discard) consoleDebugging := zapcore.Lock(os.Stdout) consoleErrors := zapcore.Lock(os.Stderr) kafkaEncoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()) core := zapcore.NewTee( zapcore.NewCore(kafkaEncoder, topicErrors, highPriority), zapcore.NewCore(consoleEncoder, consoleErrors, highPriority), zapcore.NewCore(kafkaEncoder, topicDebugging, lowPriority), zapcore.NewCore(consoleEncoder, consoleDebugging, lowPriority), ) logger := zap.New(core) defer logger.Sync() logger.Info("constructed a logger" ) }
AI解析 这段代码主要实现了一个使用 Uber 的 zap 日志库的多目的地、多级别日志系统。它将不同级别的日志同时输出到控制台和 Kafka 主题,确保错误日志得到更妥善的处理。
核心功能和实现步骤
日志级别分离 :
定义了两个优先级过滤器:
highPriority
:处理 Error 及以上级别的日志
lowPriority
:处理低于 Error 级别的日志(Info、Debug 等)
输出目的地配置 :
Kafka 主题
:配置了两个 Kafka 主题(示例中使用
作为占位符):
topicDebugging
:用于低优先级日志
topicErrors
:用于高优先级日志
控制台输出
:
consoleDebugging
:标准输出(stdout)
consoleErrors
:标准错误输出(stderr)
编码器设置 :
JSON 编码器 :用于 Kafka 输出,便于机器解析
控制台编码器 :用于终端输出,采用人类可读的格式
核心配置组合 :
使用
组合多个日志核心,实现:
错误级别的日志同时输出到 Kafka 的 errors 主题和控制台错误输出
非错误级别的日志同时输出到 Kafka 的 debugging 主题和控制台标准输出
日志使用 :
创建完成的日志实例支持所有标准 zap 方法(如 Info、Error 等)
使用defer logger.Sync()
确保程序退出前所有日志都被刷新
代码关键点解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 highPriority := zap.LevelEnablerFunc(func (lvl zapcore.Level) bool { return lvl >= zapcore.ErrorLevel }) topicDebugging := zapcore.AddSync(io.Discard) topicErrors := zapcore.AddSync(io.Discard) core := zapcore.NewTee( zapcore.NewCore(kafkaEncoder, topicErrors, highPriority), zapcore.NewCore(consoleEncoder, consoleErrors, highPriority), zapcore.NewCore(kafkaEncoder, topicDebugging, lowPriority), zapcore.NewCore(consoleEncoder, consoleDebugging, lowPriority), )
实际应用注意事项
代码中使用io.Discard
作为 Kafka 输出的占位符,实际使用时需要替换为真正的 Kafka 写入器
对于高并发场景,zapcore.Lock
确保了控制台输出的线程安全
不同环境(开发 / 生产)可以通过调整编码器配置优化输出格式
defer logger.Sync()
是关键的,确保程序退出前所有缓冲的日志都被写入
这种多目的地、多级别日志系统特别适合需要同时进行实时监控和事后分析的生产环境。
4.4 例子3:日志写入文件
与上面例子2相似,但是比它简单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package mainimport ( "os" "go.uber.org/zap" "go.uber.org/zap/zapcore" )func main () { writetofile() }func writetofile () { config := zap.NewProductionEncoderConfig() config.EncodeTime = zapcore.ISO8601TimeEncoder fileEncoder := zapcore.NewJSONEncoder(config) defaultLogLevel := zapcore.DebugLevel logFile, _ := os.OpenFile("./log-test-zap.json" , os.O_WRONLY|os.O_CREATE|os.O_APPEND, 06666 ) writer := zapcore.AddSync(logFile) logger := zap.New( zapcore.NewCore(fileEncoder, writer, defaultLogLevel), zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel), ) defer logger.Sync() url := "http://www.test.com" logger.Info("write log to file" , zap.String("url" , url), zap.Int("attemp" , 3 ), ) }
4.5 例子4:根据日志级别写入不同文件
这个与上面例子2相似
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package mainimport ( "os" "go.uber.org/zap" "go.uber.org/zap/zapcore" )func main () { writeToFileWithLogLevel() }func writeToFileWithLogLevel () { config := zap.NewProductionEncoderConfig() config.EncodeTime = zapcore.ISO8601TimeEncoder fileEncoder := zapcore.NewJSONEncoder(config) logFile, _ := os.OpenFile("./log-debug-zap.json" , os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666 ) errFile, _ := os.OpenFile("./log-err-zap.json" , os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666 ) teecore := zapcore.NewTee( zapcore.NewCore(fileEncoder, zapcore.AddSync(logFile), zap.DebugLevel), zapcore.NewCore(fileEncoder, zapcore.AddSync(errFile), zap.ErrorLevel), ) logger := zap.New(teecore, zap.AddCaller()) defer logger.Sync() url := "http://www.diff-log-level.com" logger.Info("write log to file" , zap.String("url" , url), zap.Int("time" , 3 ), ) logger.With( zap.String("url" , url), zap.String("name" , "jimmmyr" ), ).Error("test error " ) }
主要是设置日志级别,和把 2 个设置的 NewCore 放入到方法 NewTee 中。
4.6 日志切割文档
lumberjack 这个库是按照日志大小切割日志文件。
安装 v2 版本:(推荐)
1 go get github.com/natefinch/lumberjack@v2
1 2 3 4 5 6 7 log.SetOutput(&lumberjack.Logger{ Filename: "/var/log/myapp/foo.log" , MaxSize: 500 , MaxBackups: 3 , MaxAge: 28 , Compress: true , })
参照它的文档和结合上面自定义配置的例子,写一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package mainimport ( "fmt" "go.uber.org/zap" "go.uber.org/zap/zapcore" "gopkg.in/natefinch/lumberjack.v2" )func main () { lumberjacklogger := &lumberjack.Logger{ Filename: "./log-rotate-test.json" , MaxSize: 1 , MaxBackups: 3 , MaxAge: 28 , Compress: true , } defer lumberjacklogger.Close() config := zap.NewProductionEncoderConfig() config.EncodeTime = zapcore.ISO8601TimeEncoder fileEncoder := zapcore.NewJSONEncoder(config) core := zapcore.NewCore( fileEncoder, zapcore.AddSync(lumberjacklogger), zap.InfoLevel, ) logger := zap.New(core) defer logger.Sync() for i := 0 ; i < 8000 ; i++ { logger.With( zap.String("url" , fmt.Sprintf("www.test%d.com" , i)), zap.String("name" , "jimmmyr" ), zap.Int("age" , 23 ), zap.String("agradege" , "no111-000222" ), ).Info("test info " ) } }
五、日志级别和实践
5.1 日志级别
Zap 定义了 7 个日志级别,按严重程度从低到高排列:
1 DebugLevel → InfoLevel → WarnLevel → ErrorLevel → DPanicLevel → PanicLevel → FatalLevel
各级别适用场景:
Debug :开发调试细节,生产环境通常禁用
Info :常规运行状态信息(如启动、配置加载)
Warn :潜在问题(如配置过时、资源接近耗尽)
Error :可恢复错误(如网络临时中断)
DPanic :开发环境会触发 panic,生产环境仅记录
Panic :记录后触发 panic(调用panic()
)
Fatal :记录后终止程序(调用os.Exit(1)
)
5.2 核心打印逻辑规则
级别启用规则
启用某个级别的日志时,高于它的所有级别日志也会被包含 。
例如:
设置为InfoLevel
→ 实际输出Info/Warn/Error/DPanic/Panic/Fatal
设置为ErrorLevel
→ 实际输出Error/DPanic/Panic/Fatal
过滤器优先级
当多个LevelEnabler
存在时,日志会被每个匹配的输出目标接收。例如:
1 2 3 4 core := zapcore.NewTee( zapcore.NewCore(fileEncoder, errorFile, ErrorLevel), zapcore.NewCore(fileEncoder, allFile, InfoLevel), )
Error
日志会同时写入errorFile
和allFile
Info
日志只会写入allFile
输出目标隔离
通过zapcore.NewTee
可以将不同级别的日志定向到不同目的地:
1 2 3 4 5 6 7 core := zapcore.NewTee( zapcore.NewCore(kafkaEncoder, alertTopic, ErrorLevel), zapcore.NewCore(fileEncoder, errorFile, ErrorLevel), zapcore.NewCore(fileEncoder, infoFile, InfoLevel), )
5.3 常见配置模式
5.3.1 按级别拆分文件
1 2 3 4 5 core := zapcore.NewTee( zapcore.NewCore(fileEncoder, debugFile, DebugLevel), zapcore.NewCore(fileEncoder, infoFile, InfoLevel), zapcore.NewCore(fileEncoder, errorFile, ErrorLevel), )
debug.log
:仅包含 Debug 日志
info.log
:包含 Info/Warn/Error/…
error.log
:包含 Error/DPanic/…
5.3.2 生产环境典型配置
1 2 3 4 5 6 7 8 core := zapcore.NewTee( zapcore.NewCore(jsonEncoder, errorFile, ErrorLevel), zapcore.NewCore(kafkaEncoder, alertTopic, ErrorLevel), zapcore.NewCore(consoleEncoder, os.Stdout, InfoLevel), )
5.3.3 严格分离级别
1 2 3 4 5 6 7 8 9 core := zapcore.NewTee( zapcore.NewCore(fileEncoder, debugFile, zap.LevelEnablerFunc(func (lvl zapcore.Level) bool { return lvl == zapcore.DebugLevel })), zapcore.NewCore(fileEncoder, infoFile, zap.LevelEnablerFunc(func (lvl zapcore.Level) bool { return lvl == zapcore.InfoLevel })), )
5.4 最佳实践建议
合理设置默认级别
开发环境:DebugLevel
测试环境:InfoLevel
生产环境:WarnLevel
或ErrorLevel
关键错误增加上下文
1 2 3 4 5 logger.Error("数据库连接失败" , zap.String("host" , dbHost), zap.Int("port" , dbPort), zap.Error(err), )
动态调整级别
使用zap.AtomicLevel
允许运行时调整日志级别:
1 2 3 4 5 6 7 level := zap.NewAtomicLevelAt(zap.InfoLevel) logger := zap.New(core, zap.WrapCore(func (core zapcore.Core) zapcore.Core { return level.LevelEnabler(core) })) level.SetLevel(zap.DebugLevel)
避免性能损耗 (很有用)
对于高开销操作,使用条件日志:
1 2 3 if logger.Core().Enabled(zap.DebugLevel) { logger.Debug("计算结果" , zap.Int("result" , expensiveCalculation())) }
5.5 常见误区
误认为日志会重复输出
只有当多个输出目标匹配同一级别时才会重复
通过合理设计LevelEnabler
可避免重复
过度使用高级别日志
仅在真正需要立即关注的情况下使用Fatal
/Panic
频繁的Panic
/Fatal
会导致系统不稳定
忽略 DPanic 的特殊作用
在生产环境中,DPanic
表现得像Error
在测试 / 开发环境中,DPanic
会触发崩溃,帮助发现潜在问题
六、生产/开发环境控制
6.1 环境配置核心差异
Zap 通过 显式配置选项 区分生产与开发环境,而非自动检测。核心差异体现在:
特性
生产环境(默认)
开发环境
创建方式
zap.NewProduction()
zap.NewDevelopment()
DPanic 行为
仅记录日志,不触发 panic
记录日志后触发 panic
日志格式
JSON 格式(便于机器解析)
控制台格式(更易读)
堆栈跟踪
仅 Error 及以上级别包含堆栈
自动为 Warn 及以上级别添加堆栈
默认日志级别
InfoLevel
DebugLevel
6.2 环境配置实现方式
基础环境创建
1 2 3 4 5 6 7 prodLogger, _ := zap.NewProduction()defer prodLogger.Sync() devLogger, _ := zap.NewDevelopment()defer devLogger.Sync()
通过配置对象手动控制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func newLogger (isDevelopment bool ) (*zap.Logger, error ) { var config zap.Config if isDevelopment { config = zap.NewDevelopmentConfig() config.Level.SetLevel(zap.DebugLevel) } else { config = zap.NewProductionConfig() config.Level.SetLevel(zap.InfoLevel) } if !isDevelopment { config.OutputPaths = []string {"logs/app.log" } } logger, err := config.Build() if err != nil { return nil , err } return logger, nil }
环境变量控制示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func main () { env := os.Getenv("APP_ENV" ) var logger *zap.Logger var err error if env == "development" { logger, err = zap.NewDevelopment() } else { logger, err = zap.NewProduction() } if err != nil { panic (err) } defer logger.Sync() }
6.3 DPanic 在不同环境的行为
生产环境 :仅记录日志,不触发 panic,适合标记 “理论上不应发生但可容忍” 的错误。
开发环境 :记录日志后触发 panic,强制暴露潜在问题,帮助开发阶段快速定位错误。
1 2 logger.DPanic("数据校验失败,这在正常流程中不应发生" )
6.4 环境相关的最佳实践
日志级别动态调整
使用 zap.AtomicLevel
允许运行时调整日志级别,无需重启服务:
1 2 3 4 5 6 7 8 9 level := zap.NewAtomicLevelAt(zap.InfoLevel) logger := zap.New( zapcore.NewCore( encoder, writer, level, ), ) level.SetLevel(zap.DebugLevel)
生产环境的性能优化
开发环境的调试增强
启用控制台格式和完整堆栈跟踪:
1 2 3 4 devLogger, _ := zap.NewDevelopment( zap.AddStacktrace(zapcore.DebugLevel), zap.AddCaller(), )
6.5 注意事项
避免硬编码环境判断 :通过配置文件或环境变量管理环境,而非代码内直接判断。
生产环境慎用 DPanic :除非明确需要在生产环境记录但不触发 panic 的场景。
日志同步保障 :使用 defer logger.Sync()
确保程序退出前日志刷新,避免丢失。
6.6 典型配置对比
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 func initProdLogger () *zap.Logger { logger, _ := zap.NewProduction( zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel), zap.WriteSyncer(&lumberjack.Logger{ Filename: "logs/app.log" , MaxSize: 100 , MaxBackups: 30 , MaxAge: 28 , }), ) return logger }func initDevLogger () *zap.Logger { logger, _ := zap.NewDevelopment( zap.AddCaller(), zap.AddStacktrace(zapcore.DebugLevel), zap.AddConsoleEncoder(func (cfg *zapcore.EncoderConfig) { cfg.EncodeTime = zapcore.ISO8601TimeEncoder }), ) return logger }
通过合理配置环境差异,Zap 可在开发阶段快速暴露问题,同时在生产环境保证性能与稳定性。
七、其它方法使用
7.1 全局 Logger
zap提供了 2 种全局 Logger,一个是 zap.Logger,调用 zap.L() 获取;
另外一个是 zap.SugaredLogger ,调用 zap.S() 获取。
注意:直接调用 zap.L() 或 zap.S() 记录日志的话,它是不会记录任何日志信息。需要调用 ReplaceGlobals() 函数将它设置为全局 Logger。
ReplaceGlobals 替换全局 Logger 和 SugaredLogger,并返回一个函数来恢复原始值。
并发使用它是安全的。
看看 zap/global.go 中的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 var ( _globalMu sync.RWMutex _globalL = NewNop() _globalS = _globalL.Sugar() )func L () *Logger { _globalMu.RLock() l := _globalL _globalMu.RUnlock() return l }func S () *SugaredLogger { _globalMu.RLock() s := _globalS _globalMu.RUnlock() return s }func ReplaceGlobals (logger *Logger) func () { _globalMu.Lock() prev := _globalL _globalL = logger _globalS = logger.Sugar() _globalMu.Unlock() return func () { ReplaceGlobals(prev) } }
上面源码中的关键是 _globalL = NewNop() , NewNop 函数源码在 zap/logger.go 中,这个函数返回初始化了的一个 *Logger:
1 2 3 4 5 6 7 8 9 10 func NewNop () *Logger { return &Logger{ core: zapcore.NewNopCore(), errorOutput: zapcore.AddSync(io.Discard), addStack: zapcore.FatalLevel + 1 , clock: zapcore.DefaultClock, } }
上面是源码简析,下面给出一个简单使用的例子。
简单使用例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport ( "go.uber.org/zap" )func main () { zap.L().Info("no log info" ) zap.S().Info("no log info [sugared]" ) logger := zap.NewExample() defer logger.Sync() zap.ReplaceGlobals(logger) zap.L().Info("log info" ) zap.S().Info("log info [sugared]" ) }
运行输出:
1 2 {"level" :"info" ,"msg" :"log info" } {"level" :"info" ,"msg" :"log info [sugared]" }
7.2 与标准日志库搭配
zap 提供了一个函数 NewStdLog
,可以把标准日志库 log 转换为 zap 的日志,这为我们从标准日志库转换到 zap 日志库的使用提供了简洁的转换操作。
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport ( "go.uber.org/zap" )func main () { logger := zap.NewExample() defer logger.Sync() std := zap.NewStdLog(logger) std.Print("standard logger wrapper" ) }
运行输出:
1 {"level" :"info" ,"msg" :"standard logger wrapper" }
如果你还想设置日志级别,可以使用另外一个函数 NewStdLogAt
,它的第二个参数就是日志级别:
1 NewStdLogAt(l *Logger, level zapcore.Level) (*log.Logger, error )
一段代码中使用log另外的使用zap
zap 还提供了另外一个函数 RedirectStdLog
,它可以帮助我们在一段代码中使用标准日志库 log,其它地方还是使用 zap.Logger
。如下例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport ( "log" "go.uber.org/zap" )func main () { logger := zap.NewExample() defer logger.Sync() undo := zap.RedirectStdLog(logger) log.Print("redirected standard library" ) undo() log.Print("this zap logger" ) }
输出:
1 2 {"level" :"info" ,"msg" :"redirected standard library" }2023 /05 /06 00 :47 :11 this zap logger
同样如果想增加日志级别,可以使用函数 RedirectStdLogAt
:
1 func RedirectStdLogAt (l *Logger, level zapcore.Level) (func () , error )
7.3 输出调用堆栈
主要是调用函数 zap.AddStacktrace()
,见下面例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package mainimport ( "go.uber.org/zap" "go.uber.org/zap/zapcore" )func Hello () { Warn("hello" , zap.String("h" , "world" ), zap.Int("c" , 1 )) }func Warn (msg string , fields ...zap.Field) { zap.L().Warn(msg, fields...) }func main () { logger, _ := zap.NewProduction(zap.AddStacktrace(zapcore.WarnLevel)) defer logger.Sync() zap.ReplaceGlobals(logger) Hello() }
运行输出:
1 2 3 4 5 6 {"level" :"warn" ,"ts" :1683306442.3578277 ,"caller" :"zapdemos/addstacktrace.go:13" ,"msg" :"hello" ,"h" :"world" ,"c" :1 ,"stacktrace" :"main.Warn\n\tD:/work/mygo/go-exercises/zapdemos/addstacktrace.go:13\n main.Hello\n\tD:/work/mygo/go-exercises/zapdemos/addstacktrace.go:9\n main.main\n\tD:/work/mygo/go-exercises/zapdemos/addstacktrace.go:22\n runtime.main\n\tE:/programfile/go/src/runtime/proc.go:250" }
7.4 输出文件名和行号
AddCaller 将 Logger 配置为使用 zap 调用者的文件名、行号和函数名称,把这些信息添加到日志记录中。它底层调用的是 WithCaller
。
addcaller.go:
1 2 3 4 5 6 7 8 9 10 11 12 package mainimport ( "go.uber.org/zap" )func main () { logger, _ := zap.NewProduction(zap.AddCaller()) defer logger.Sync() logger.Info("AddCaller:line No and filename" ) }
输出:
1 {"level" :"info" ,"ts" :1683307204.6184027 ,"caller" :"zapdemos/addcaller.go:11" ,"msg" :"AddCaller:line No and filename" }
logger.Info () 方法在第11行被调用。
zap 还提供了另外一个函数 zap.AddCallerSkip(skip int) Option
,可以设置向上跳几层,然后记录文件名和行号。向上跳几层就是跳过调用者的数量。有时函数调用可能有嵌套,用这个函数可以定位到里面的函数。
addcallerskip.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport ( "go.uber.org/zap" )func main () { logger, _ := zap.NewProduction(zap.AddCaller(), zap.AddCallerSkip(1 )) defer logger.Sync() zap.ReplaceGlobals(logger) Hello() }func Hello () { Warn("hello" , zap.String("h" , "world" ), zap.Int("c" , 1 )) }func Warn (msg string , fields ...zap.Field) { zap.L().Warn(msg, fields...) }
输出:
1 {"level" :"warn" ,"ts" :1683308118.1684704 ,"caller" :"zapdemos/addcallerskip.go:17" ,"msg" :"hello" ,"h" :"world" ,"c" :1 }
日志中的 17 表示 Hello() 函数里的 Warn() 的行号。
如果 zap.AddCallerSkip(2)
,日志中显示行号为 13,表示 Hello() 的行号。
九、zap使用总结
zap 的使用,先创建 logger,再调用各个日志级别方法记录日志信息。比如 logger.Info ()。
zap 提供了三种快速创建 logger 的方法: zap.Newproduction()
,zap.NewDevelopment()
,zap.NewExample()
。见名思义,Example 一般用在测试代码中,Development 用在开发环境中,Production 用在生成环境中。这三种方法都预先设置好了配置信息。它们的日志数据类型输出都是强类型。
当然,zap 也提供了给用户自定义的方法 zap.New()
。比如用户可以自定义一些配置信息等。
在上面的例子中,几乎都有 defer logger.Sync()
这段代码,为什么?因为 zap 底层 API 允许缓冲日志以提高性能,在默认情况下,日志记录器是没有缓冲的。但是在进程退出之前调用 Sync()
方法是一个好习惯。
如果你在 zap 中使用了 sugaredlogger ,把 zap 创建 logger 的三种方法用 logger.Sugar()
包装下,那么 zap 就支持 printf 风格的格式化输出,也支持以 w 结尾的方法。如 Infow,Infof 等。这种就是通用类型日志输出,不是强类型输出,不需要强制指定输出的数据类型。它们的性能区别,通用类型会比强类型下降 50% 左右。
比如 Infow 的输出形式,Infow 不需要 zap.String 这种指定字段的数据类型。如下代码:
1 2 3 4 5 6 sugar := logger.Sugar() sugar.Infow("failed to fetch URL" , "url" , url, "attempt" , 3 , "backoff" , time.Second, )
强类型输出,比如 Info 方法输出字段和值就需要指定数据类型:
1 2 3 4 5 6 logger.Info("failed to fetch url" , zap.String("url" , "http://example.com" ), zap.Int("attempt" , 3 ), zap.Duration("backoff" , time.Second), )
强类型 输出和通用类型 输出区别
通用类型输出,经过 interface{} 转换会有性能损失,标准库的 fmt.Printf 为了通用性就用了 interface{} 这种”万能型“的数据类型,另外它还使用了反射,性能进一步降低。
zap 强类型输出,zap 为了提供日志输出性能,zap 的强类型输出没有使用 interface{} 和反射。zap 默认输出就是强类型。
上面介绍,zap 中 3 种创建 logger 方式(zap.Newproduction()
,zap.NewDevelopment()
,zap.NewExample()
)就是强类型日志字段,当然,也可以转化为通用类型,用 logger.Sugar()
方法创建 SugaredLogger。
zap.Namespace()
创建一个命名空间,后面的字段都在这名字空间中。Namespace 就像一个文件夹,后面文件都放在这个文件夹里。
1 2 3 4 5 logger.Info("some message" , zap.Namespace("shop" ), zap.String("shopid" , "s1234323" ), )
https://www.doubao.com/thread/w606f7e304a7a23f1