架构总览:双 API 与三层解耦
💡 本系列全部文章以 uber-go/zap v1.27 为蓝本。阅读前建议先 clone 源码,对照着看效果更好。
zap 是 Go 生态中最流行的结构化日志库之一。它最常被引用的卖点是「性能」——但性能只是结果,架构才是原因。这一篇先建立整体心智模型,后面的文章再逐层展开。
双 API:zap 与 zapcore
打开 zap 的源码,你会发现它实际上暴露了两层 API:
zap包:面向使用者的高级 API。zap.NewProduction()、logger.Info("msg", zap.Int("n", 1))都在这一层。zapcore包:面向实现者的内核 SPI。Core、Encoder、WriteSyncer、Level、Field、Entry都在这一层。
API 是你调用框架,SPI 是框架调用你。 zap 顶层通过 zapcore 定义的接口工作,任何满足这些接口的实现都能插入其中——无论是写文件、写 Kafka 还是做指标统计。
这种双层设计带来的直接好处是:上层稳定,下层可替换。业务代码只依赖 zap 的薄封装,日志引擎的替换(比如从 JSON 换成自定义格式)不需要改动任何调用方。
三层职责:过滤、编码、写入
一条日志从产生到落盘,要经过三个独立的阶段:
gozapcore/core.go
type Core interface {
Enabled(Level) bool
With([]Field) Core
Check(Entry, *CheckedEntry) *CheckedEntry
Write(Entry, []Field) error
Sync() error
}
三个阶段分别由不同的抽象承担:
| 阶段 | 抽象 | 职责 |
|---|---|---|
| 过滤 | Level + Core.Enabled |
决定这条日志是否需要处理 |
| 编码 | Encoder |
把结构化的 Entry/Field 变成字节 |
| 写入 | WriteSyncer |
把字节写到目的地(文件、网络、终端) |
三者通过 Core 串联:Check 负责过滤与检查,Write 负责编码并落盘。
解耦带来的扩展性
正因为三层严格解耦,zap 的扩展都是「组合」而非「修改」:
- 想同时写文件和 Kafka?用
multiCore组合两个 Core。 - 想在高频路径上采样?用
sampler装饰任意 Core。 - 想输出到自定义目的地?实现一个
WriteSyncer即可。
三层解耦是 zap 的强项,但也是它代码量偏大的原因。如果你的场景只是「打几条日志」,标准库 log/slog 可能更合适——架构选择永远服务于需求规模。
一条日志的完整旅程
Logger.Info("msg", fields...)
│
├─ 1. 构建 Entry(时间、级别、消息、调用栈)
├─ 2. core.Check() → 级别过滤 + 采样判断
├─ 3. core.Write() → encoder.EncodeEntry() 编码为 JSON
└─ 4. writeSyncer.Write() → 写入输出目的地
后面几篇会沿着这条路径逐个放大:zapcore SPI 讲接口本身,Encoder 体系 讲编码,Field 系统 讲字段如何做到零反射,AtomicLevel 讲动态配置,采样器 讲高频路径优化。
小结
架构师看开源项目,第一件事不是读代码,而是找分层。zap 的分层清晰到可以用一句话概括:用接口隔离变化,用组合实现扩展。理解了这句话,zap 的源码就只剩细节了。