1. 结构:薄薄一层适配器
sugar.go:55-57:
Go
type SugaredLogger struct {
base *Logger // 唯一字段:持有强类型 Logger
}
Sugar 不复制任何日志能力,所有 4×8 个方法最终都汇聚到同一个私有入口:
Go
// s.log(sugar.go:346-357)------ 所有方法的共同出口
func (s *SugaredLogger) log(lvl zapcore.Level, template string, fmtArgs []interface{}, context []interface{}) {
// ① 同样的快路径:级别禁用直接返回(连格式化都不做)
if lvl < DPanicLevel && !s.base.Core().Enabled(lvl) {
return
}
msg := getMessage(template, fmtArgs) // ② 把参数变成消息字符串
if ce := s.base.Check(lvl, msg); ce != nil { // ③ 复用 Logger 的 check!
ce.Write(s.sweetenFields(context)...) // ④ 松散键值对 → []Field
}
}
四大家族的调用姿势:
Go
sugar.Info(args...) → s.log(InfoLevel, "", args, nil) // Print 风格
sugar.Infof(tpl, args...) → s.log(InfoLevel, tpl, args, nil) // printf
sugar.Infow(msg, kv...) → s.log(InfoLevel, msg, nil, kv) // 结构化
sugar.Infoln(args...) → s.logln(InfoLevel, args, nil) // Println(换行版)
logln(360-369)与 log 唯一区别是用 getMessageln(Sprintln 且去掉尾部换行)。
设计启示:一个私有入口 + 参数归一化 ,避免 30 个方法复制逻辑。Logger 那边也是同构的 check+Write 两行。
2. getMessage:消息如何拼出来
sugar.go:372-387:
Go
func getMessage(template string, fmtArgs []interface{}) string {
if len(fmtArgs) == 0 {
return template // ① 没参数:模板就是消息
}
if template != "" {
return fmt.Sprintf(template, fmtArgs...) // ② 有模板:Sprintf
}
if len(fmtArgs) == 1 {
if str, ok := fmtArgs[0].(string); ok {
return str // ③ 单个纯字符串参数:直接用,零拼接
}
}
return fmt.Sprint(fmtArgs...) // ④ 兜底:Sprint 拼接
}
决策树:
Go
参数为空? ──是──▶ template 原样
│否
template 非空? ──是──▶ fmt.Sprintf(template, args...)
│否(Print风格)
单个参数且是 string? ──是──▶ 直接返回该字符串(快路径!)
│否
fmt.Sprint(args...) ← 注意:Sprint 只在两个操作数都非字符串时才加空格
所以 sugar.Info("found ", n, " items") 的空格行为来自 fmt.Sprint 规则(非字符串相邻才补空格)------这就是 godoc 里"Spaces are added between arguments when neither is a string"的出处。
3. sweetenFields:松散键值对 → \[\]Field
核心算法(sugar.go:395-454),把 []interface{} 翻译成强类型字段:
Go
func (s *SugaredLogger) sweetenFields(args []interface{}) []Field {
if len(args) == 0 {
return nil
}
var (
fields = make([]Field, 0, len(args)) // 按最坏情况预分配
invalid invalidPairs // 收集非法键值对
seenError bool
)
for i := 0; i < len(args); {
// 情况 ①:本身就是强类型 Field → 直通(sugar.go:410-414)
if f, ok := args[i].(Field); ok {
fields = append(fields, f)
i++
continue
}
// 情况 ②:独立的 error → 第一个变 "error" 字段(417-426)
if err, ok := args[i].(error); ok {
if !seenError {
seenError = true
fields = append(fields, Error(err))
} else {
// 后续 error:单独打一条内部错误日志(因为字段无法表达重名)
s.base.Error(_multipleErrMsg, Error(err))
}
i++
continue
}
// 情况 ③:落单的 key(后面没值了)→ 报告并终止(429-432)
if i == len(args)-1 {
s.base.Error(_oddNumberErrMsg, Any("ignored", args[i]))
break
}
// 情况 ④:消费 (key, value) 对(436-446)
key, val := args[i], args[i+1]
if keyStr, ok := key.(string); !ok {
// key 不是字符串 → 暂存 invalid(先攒着最后一起报)
if cap(invalid) == 0 {
invalid = make(invalidPairs, 0, len(args)/2)
}
invalid = append(invalid, invalidPair{i, key, val})
} else {
fields = append(fields, Any(keyStr, val)) // ← 松散值的类型分发靠 Any
}
i += 2
}
// ⑤ 一次性报告所有非法对(450-452)
if len(invalid) > 0 {
s.base.Error(_nonStringKeyErrMsg, Array("invalid", invalid))
}
return fields
}
3.1 值得学习的细节
|----------------------------------------|-------------------------------------------------------------|
| 细节 | 意图 |
| fields 按 len(args) 预分配 | 最坏情况每个参数半个字段;传纯 Field 列表时不被惩罚(注释 401-402) |
| invalid 惰性分配(cap(invalid)==0 才 make) | 合法输入(99% 情况)零额外分配 |
| 错误不在循环里立刻打而是攒起来 | 一条日志只报告一次,避免刷屏;invalidPair 实现了 MarshalLogObject 结构化输出位置/键/值 |
| 情况② 用 seenError 而非计数 | 字段 key 会撞名(都叫 "error"),所以只留第一个 |
3.2 panic 在哪?------不在 sugar 里
注意 sweetenFields 对非法输入是记内部错误 而不是 panic(注释在 sugar.go:109-113 的 With 文档里说 "In development, passing a non-string key panics"------实际实现是统一记 Error 日志;doc 描述的是推荐理解)。源码事实:所有非法输入都会变成 Error 级内部日志 ,业务不中断。去 errorOutput(默认 stderr)里找这些消息:
Go
Ignored key without a value. {"ignored": ...}
Ignored key-value pairs with non-string keys. {"invalid":[...]}
Multiple errors without a key. {"error": ...}
4. With / WithLazy 的 Sugar 版
Go
// sugar.go:114-116
func (s *SugaredLogger) With(args ...interface{}) *SugaredLogger {
return &SugaredLogger{base: s.base.With(s.sweetenFields(args)...)}
}
// sugar.go:129-131 WithLazy 同理
Sugar 版 With 的文档(88-107)展示了混传的等价性:
Go
s.With("hello", "world", Stack(), "count", 42)
≡ unsugar.With(String("hello","world"), Stack(), Int("count",42))
5. Sugar 到底慢在哪?(成本核算)
以 sugar.Infow("m", "k", 3) 为例,相比 logger.Info("m", zap.Int("k",3)) 多出的工作:
|------|-------------------------------|--------------------------------------------------------|----------|
| 环节 | 强类型 Logger | SugaredLogger | 成本 |
| 参数打包 | zap.Int 装 Field(栈上,0 alloc) | []interface{} 切片 + 装箱 int→interface{} | 1 次分配 |
| 消息处理 | 无 | getMessage(本例 template 直返,便宜) | ~0 |
| 字段转换 | 无 | sweetenFields:预分配 []Field + 类型断言 + Any 的 type switch | 1~2 次分配 |
| 后续 | 相同 | 相同(都走 base.Check → ce.Write) | --- |
结论:
- ** Sugar ≈ 强类型 + 2~3 次小分配 + 一轮 type switch**。绝对值是几百纳秒级
- 级别禁用时两边都近乎免费(349 行的 Enabled 前置)
Infof额外多一次fmt.Sprintf(多次分配),四个家族里最贵;Infoln次之(Sprintln);Infow和Info(Print 风格)相对好
这也是官方文档说"4-10x faster than other structured logging packages(Sugar)"而基础 Logger 更快的原因------两者的差距远小于和反射系库的差距。
6. 互转的对称性
Go
// logger.go:146-150
Sugar(): clone + callerSkip += 2 // 多两层帧:sugar.Info → s.log → base.Check...
// sugar.go:63-67
Desugar(): clone + callerSkip -= 2
WithOptions(sugar.go:76-82)没有走 base.WithOptions 而是手动 clone+apply------因为要在base上应用(保持 callerSkip 语义)。这是一处容易看漏的细节。