1. 结构:薄薄一层适配器
sugar.go:55-57:
type SugaredLogger struct { base *Logger // 唯一字段:持有强类型 Logger }Sugar 不复制任何日志能力,所有 4×8 个方法最终都汇聚到同一个私有入口:
// 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 } }四大家族的调用姿势:
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:
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 拼接 }决策树:
参数为空? ──是──▶ 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{}翻译成强类型字段:
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 值得学习的细节
细节 | 意图 |
| 最坏情况每个参数半个字段;传纯 Field 列表时不被惩罚(注释 401-402) |
invalid 惰性分配( | 合法输入(99% 情况)零额外分配 |
错误不在循环里立刻打而是攒起来 | 一条日志只报告一次,避免刷屏;invalidPair 实现了 MarshalLogObject 结构化输出位置/键/值 |
情况② 用 | 字段 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)里找这些消息:
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 版
// 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)展示了混传的等价性:
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 | 成本 |
参数打包 |
|
| 1 次分配 |
消息处理 | 无 | getMessage(本例 template 直返,便宜) | ~0 |
字段转换 | 无 | sweetenFields:预分配 | 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. 互转的对称性
// logger.go:146-150 Sugar(): clone + callerSkip += 2 // 多两层帧:sugar.Info → s.log → base.Check... // sugar.go:63-67 Desugar(): clone + callerSkip -= 2WithOptions(sugar.go:76-82)没有走base.WithOptions而是手动 clone+apply——因为要在base上应用(保持 callerSkip 语义)。这是一处容易看漏的细节。