Go Zap 怎样把不同等级日志分流到两个文件
Zap 按等级分流到两个文件,需要为两个 Core 分别设置等级条件,再用 zapcore.NewTee 组合。若希望 Info、Warn 只进入 app.log,Error 及以上只进入 error.log,普通日志条件必须同时设置下界和上界;仅把普通 Core 的最低等级设为 Info,会让 Error 也被它接受。
Zap 官方项目:https://github.com/uber-go/zap
数据来源:先把业务记录和日志系统错误分开
以一个后台任务服务为例,任务接收与等待属于普通记录,处理失败属于错误记录。维护者希望日常检索先看 app.log,失败定位先看 error.log,同时用 task_id 关联同一项任务。这里要求的是互斥分流,而不是把错误额外复制一份。
还有一种容易混淆的错误:文件写入失败、磁盘异常等来自日志系统本身。Zap 的 ErrorOutput 用于报告这类内部错误,它与调用 logger.Error 生成的业务记录不是同一个出口。把两个概念分开,后面遇到错误日志缺失时才知道应到哪里找原因。
等级判断:NewTee 组合 Core,不替你做互斥选择
NewTee 不是“第一个匹配就停止”的路由器。各个 Core 独立判断记录等级,条件重叠就可能同时接收;条件之间有空隙,某些等级就没有对应出口。本文选定两个范围:普通分支接受 Info 到 Error 之前的等级,即 Info、Warn;错误分支接受 Error 及以上;Debug 不写入这两个文件。
可以把结构分成日志入口组、普通分支组和错误分支组。日志入口组包含 Logger 与 NewTee,普通分支组包含普通 Core 与 app.log,错误分支组包含错误 Core 与 error.log。Logger 使用组合 Core,每个分支各自连接文件,等级标签说明各自接收范围。

存储模型:每个文件绑定自己的编码器和写入器
下面给出完整初始化代码。两个文件使用追加写入;每个 Core 单独创建 JSON 编码器,再把文件包装成带锁的 WriteSyncer。代码通过 errors.Join 汇总收尾错误,需要 Go 1.20 或以上版本;项目还应满足自己选定的 Zap 版本要求。
package main
import (
"errors"
"fmt"
"os"
"path/filepath"
"sync"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func newSplitLogger(dir string) (*zap.Logger, func() error, error) {
// 示例目录和文件使用较收敛的权限;已有文件的权限不会由 OpenFile 重设。
if err := os.MkdirAll(dir, 0700); err != nil {
return nil, nil, fmt.Errorf("创建日志目录: %w", err)
}
appFile, err := os.OpenFile(filepath.Join(dir, "app.log"),
os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
return nil, nil, fmt.Errorf("打开普通日志: %w", err)
}
errorFile, err := os.OpenFile(filepath.Join(dir, "error.log"),
os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
// 第二个文件打开失败,也要收回已经打开的第一个文件。
return nil, nil, errors.Join(fmt.Errorf("打开错误日志: %w", err), appFile.Close())
}
appLevels := zap.LevelEnablerFunc(func(level zapcore.Level) bool {
// 用上下界排除 Error,避免两个 Core 的等级范围重叠。
return level >= zapcore.InfoLevel && level = zapcore.ErrorLevel
})
cfg := zap.NewProductionEncoderConfig()
cfg.EncodeTime = zapcore.ISO8601TimeEncoder
// 每个 Core 创建独立编码器;文件写入器按 Zap 的要求包装并发锁。
appCore := zapcore.NewCore(zapcore.NewJSONEncoder(cfg),
zapcore.Lock(zapcore.AddSync(appFile)), appLevels)
errorCore := zapcore.NewCore(zapcore.NewJSONEncoder(cfg),
zapcore.Lock(zapcore.AddSync(errorFile)), errorLevels)
logger := zap.New(zapcore.NewTee(appCore, errorCore),
zap.AddCaller(),
// 这里输出的是日志系统自身的错误,不是业务 Error 日志。
zap.ErrorOutput(zapcore.Lock(zapcore.AddSync(os.Stderr))))
var once sync.Once
var closeErr error
cleanup := func() error {
once.Do(func() {
// 调用前先停止所有业务写入;先同步,再关闭两个文件。
closeErr = errors.Join(logger.Sync(), appFile.Close(), errorFile.Close())
})
return closeErr
}
return logger, cleanup, nil
}
func run() error {
logger, cleanup, err := newSplitLogger("./logs")
if err != nil {
return err
}
logger.Info("任务已接收", zap.String("task_id", "demo-task"))
logger.Warn("任务进入等待", zap.String("task_id", "demo-task"))
logger.Error("任务处理失败", zap.String("task_id", "demo-task"))
// 示例没有后台写入协程,三次记录结束后即可收尾。
return cleanup()
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
示例安装与调用方式如下。本文未运行这段代码,下面的归属表是由条件表达式推导的预期行为。
# 在新的示例目录初始化模块;已有模块只需按项目策略添加依赖。 go mod init example.com/splitlog go get go.uber.org/zap # 保存 main.go 后执行;程序只在当前目录创建 logs 和两个日志文件。 go run . # 在本机查看两个文件,核对等级字段与任务标识;以下不是本文实测记录。 cat ./logs/app.log cat ./logs/error.log
这里没有直接套用生产预设构造器,而是使用自定义 Core,因此不隐含启用生产预设的采样规则。若后续加入采样,需要另外决定哪类记录允许丢弃;仅为分文件输出,不必把采样也混入第一版实现。
查询路径:看文件归属,也保留关联字段
| 等级或来源 | 本例出口 | 判断依据 |
|---|---|---|
| Debug | 两个文件都不接收 | 低于普通分支下界 |
| Info | app.log | 满足普通分支范围 |
| Warn | app.log | 仍低于 Error |
| Error | error.log | 只满足错误分支 |
| 更高等级 | 按等级进入错误分支 | 还需考虑该日志方法的 panic 或退出行为 |
| Zap 内部写入错误 | 标准错误输出 | 由 ErrorOutput 指定 |
两类文件保留同样的任务标识,才能在发现失败后回看此前的普通记录。不要把“按等级分流”当成日志关联机制;同一个 task_id、request_id 或业务事件标识仍需要由应用明确写入。
如果业务目标是 app.log 保存全部 Info 及以上记录,同时 error.log 再保存一份错误,那么允许范围重叠反而是正确设计。要先写清“互斥分流”还是“错误复制”,再决定是否给普通分支添加上界。
异常处理:区分初始化失败和运行时写入失败
初始化时,目录和两个文件都可能创建失败。示例在第二个文件打开失败时立即关闭第一个文件,并把相关错误返回给调用方,避免只构造出一半的日志系统后继续运行。
运行时,Info、Warn、Error 等常用记录方法通常不会把底层文件写入错误作为返回值交给业务调用方。示例将内部错误交给 stderr;容器或进程管理器需要真正收集这个出口,否则磁盘已满时,业务逻辑可能继续运行而日志没有完整保存。Sync 的错误也应保留并报告,不能一律忽略。
不要使用当前可能已经失效的 logger 再记录同一个底层写入故障,否则会把问题送回相同的失败出口。示例收尾失败后直接写 stderr,就是为了让故障报告与两个业务日志文件保持分离。
清理策略:先停止写入,再同步并关闭文件
Logger.Sync 负责向底层写入器请求同步,不会替应用关闭通过 os.OpenFile 打开的文件。本例由 cleanup 统一负责同步和 Close;sync.Once 保证同一个收尾函数不会重复关闭文件。这个幂等收尾不表示可以一边调用 cleanup、一边继续让业务协程写日志,应用仍应先停止并等待所有写入者。
收尾结构包含资源所有者、Logger.Sync、app 文件句柄、error 文件句柄、Close 和 stderr。资源所有者掌握两个句柄及收尾入口;同步依赖日志器中的输出对象,关闭由持有句柄的一层执行,异常报告保留独立出口。图中静态关系不能代替正文所述的收尾顺序。

示例只演示固定文件,不包含按日期或体积轮转、压缩归档、保留天数和跨进程协调。长期运行服务需要另行配置轮转写入器或明确的外部轮转协议;外部把文件改名后,已打开的句柄也不会自动改为新文件名所指的对象。不要直接把不受控字符串拼进日志路径。
只用 io.MultiWriter 能实现按等级分流吗?
它面向字节复制,不知道 Zap 的日志等级。相同字节写往多个文件属于复制;要在编码和写入之前按等级判断,应使用不同 Core 的等级条件。
logger.Fatal 能依靠 defer 完成文件关闭吗?
不要这样设计。Fatal 涉及进程退出,普通 defer 不能作为该路径的可靠清理机制。示例只调用 Info、Warn 和 Error,并通过正常返回完成 cleanup。
为什么两个 Core 不共享一个可变编码器?
每个分支各自创建编码器,更容易保持字段、格式配置和并发边界清楚。编码器不是文件句柄,写入器的锁也不能代替编码器自身的正确使用。
Go Zap 怎样通过 AtomicLevel 动态调整日志等级
- 上一篇
- Go Zap 怎样通过 AtomicLevel 动态调整日志等级
- 下一篇
- Go Lumberjack 怎样按文件大小配置日志轮转
-
- Golang · Go教程 | 11分钟前 |
- Go Flock 怎样用 TryLock 实现非阻塞进程互斥
- 317浏览 收藏
-
- Golang · Go教程 | 12分钟前 |
- Go Flock 怎样用上下文超时等待文件锁
- 113浏览 收藏
-
- Golang · Go教程 | 14分钟前 |
- Go CEL 怎样限制表达式求值的成本预算
- 419浏览 收藏
-
- Golang · Go教程 | 15分钟前 |
- Go CEL 怎样用 PartialVars 保留尚未提供的输入
- 112浏览 收藏
-
- Golang · Go教程 | 18分钟前 |
- Go CEL 怎样声明输入变量并编译筛选表达式
- 394浏览 收藏
-
- Golang · Go教程 | 20分钟前 |
- Go Expr 怎样限制表达式最终必须返回布尔值
- 479浏览 收藏
-
- Golang · Go教程 | 24分钟前 |
- Go Expr 怎样预编译表达式并复用程序对象
- 204浏览 收藏
-
- Golang · Go教程 | 24分钟前 |
- Go Expr 怎样用 Env 为表达式声明结构化输入
- 357浏览 收藏
-
- Golang · Go教程 | 27分钟前 |
- Go Doublestar 怎样用 WithNoFollow 限制符号链接遍历
- 102浏览 收藏
-
- Golang · Go教程 | 27分钟前 | Go 文件遍历 io/fs Doublestar GlobWalk
- Go Doublestar 怎样用 GlobWalk 边遍历边处理匹配项
- 422浏览 收藏
-
- Golang · Go教程 | 30分钟前 |
- Go Doublestar 怎样只匹配指定目录下的多层文件
- 331浏览 收藏
-
- Golang · Go教程 | 31分钟前 |
- Go Pond 怎样为子池划分父池的并发额度
- 241浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 773次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 770次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 852次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 821次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 651次使用
-
- Java 性能优化上线清单:从定位、改造到灰度发布
- 2026-06-11 860浏览
-
- Spring Boot 压测验证:Gatling、JMeter 与性能回归门禁
- 2026-06-11 843浏览
-
- Java NMT 非堆内存排查:Direct Buffer、线程栈与 Metaspace 分析
- 2026-06-11 826浏览
-
- Spring Boot 容器内存优化:JVM 堆、非堆与 MaxRAMPercentage
- 2026-06-11 809浏览
-
- Tomcat 连接与线程参数调优:maxThreads、acceptCount 与 KeepAlive
- 2026-06-11 792浏览

