Encoder 体系:JSON 与 Console 的取舍

上一篇文章我们认识了 Encoder 接口,这一篇深入它的两种内置实现,看看 zap 如何在「机器可读」与「人类可读」之间做取舍。

ObjectEncoder:字段编码的骨架

Encoder 组合了 ObjectEncoder,后者定义了一组 Add* 方法——这是 zap 字段编码的核心通道:

gozapcore/encoder.go
type ObjectEncoder interface {
	AddArray(key string, marshaler ArrayMarshaler) error
	AddObject(key string, marshaler ObjectMarshaler) error
	AddBinary(key string, value []byte)
	AddBool(key string, value bool)
	AddDuration(key string, value time.Duration)
	AddFloat64(key string, value float64)
	AddInt(key string, value int)
	AddString(key, value string)
	AddTime(key string, value time.Time)
	// ... 还有更多
}

每种类型对应一个方法,没有反射——这就是 zap 快的第一个秘密。

jsonEncoder:面向机器

jsonEncoder 把每个字段编码为 JSON 键值对,输出适合 Logstash、Loki 等系统解析:

json输出示例
{"level":"info","ts":1718000000.123,"caller":"main.go:23","msg":"user login","user_id":10086,"cost_ms":12}

它的实现要点:

  • 零分配追加:直接向 bytes.Buffer 追加字节,不做中间对象
  • 时间戳用 Unix 秒:浮点数比 RFC3339 字符串更省空间、更快解析
  • 引号与转义:字符串按 JSON 规则转义,但 AppendString 会做快速路径判断

consoleEncoder:面向人类

consoleEncoder 复用了 jsonEncoder 的绝大部分编码逻辑,差异只在分隔符与样式——用空格与等号代替冒号和逗号,并支持颜色:

text输出示例
2025-06-20T10:30:00.123+0800 INFO  main.go:23 user login {"user_id": 10086, "cost_ms": 12}

有趣的是,zap 用了一个很省事的技巧:consoleEncoder 在 jsonEncoder 基础上修改分隔符常量,字段值仍然以 JSON 形式输出(花括号里的部分),避免重复实现。

ReflectedEncoder:反射兜底

AddReflected 是唯一的「慢车道」——它通过反射把任意结构体序列化为 JSON。zap 对它做了两层保护:

  1. 默认使用 encoding/json,但可以注入更快的实现(如 jsoniter)
  2. 只对确实无法静态编码的字段启用,正常路径永远走 Add* 方法

⚠️ 性能红线: 高频路径上出现 zap.Any() 且值是结构体时,会触发反射。能静态编码的字段尽量用 zap.Int、zap.String 这类强类型方法。

自定义 Encoder

接入自定义格式只需两步:实现 Encoder 接口,然后用 NewCore 组装:

go自定义 Encoder 骨架
type myEncoder struct{ zapcore.Encoder }

func (e *myEncoder) Clone() zapcore.Encoder {
	return &myEncoder{e.Encoder.Clone()}
}

func NewMyCore(w zapcore.WriteSyncer, lvl zapcore.LevelEnabler) zapcore.Core {
	return zapcore.NewCore(&myEncoder{...}, w, lvl)
}

小结

Encoder 体系的价值不在于两种内置实现,而在于把「字段 → 字节」这条路径完全开放。理解 ObjectEncoder 的方法集,你就理解了 zap 高性能编码的全部秘密:类型分派、零反射、字节级追加。