架构总览:双 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 的源码就只剩细节了。