4.5 KiB
tags, aliases, date created, date modified
| tags | aliases | date created | date modified | |
|---|---|---|---|---|
|
星期三, 十二月 10日 2025, 10:37:54 晚上 | 星期三, 十二月 10日 2025, 10:38:26 晚上 |
1. 核心模式:装饰器模式的变体 (Context-Decorator Pattern)
这是我们处理 TraceID 和上下文的核心手段。
-
传统误区 (Over-Abstraction):
定义一个庞大的 MyLogger 结构体,把 zap.Logger 藏在里面,然后重写 Info, Error 等所有方法。
- 后果:维护成本极高,每次 Zap 更新或增加新特性(如
Panic或DPanic),你都得跟着改代码。且容易在转发参数时产生逃逸分析(Escape Analysis)导致的内存分配。
- 后果:维护成本极高,每次 Zap 更新或增加新特性(如
-
我们的决策 (The Thin Wrapper):
只封装“获取 Logger”的动作,不封装“Logger 本身”。
我们将定义一个函数 log.WithContext(ctx context.Context) *zap.Logger。
- 行为:这个函数极其轻量。它从
ctx中取出TraceID,调用zap.With()生成一个新的 Zap 实例并返回。 - 优势:业务代码拿到的依然是原生的
*zap.Logger。这意味着开发者可以直接使用 Zap 强大的zap.String,zap.Int等强类型字段构建方法,享受极致性能,没有任何中间层损耗。
- 行为:这个函数极其轻量。它从
2. 接口策略:拒绝通用接口 (Concrete Type Dependency)
这是 Go 语言工程实践中关于日志的一个特殊共识,也是反直觉的地方。
-
传统误区 (The Java/Interface Way):
定义一个 type ILogger interface { Info(msg string, args …interface{}) }。
- 后果:
args …interface{}会导致大量的反射(Reflection)和装箱(Boxing),这直接抹杀了 Zap 存在的意义。Zap 的核心设计哲学就是通过zap.Field避免使用interface{}。
- 后果:
-
我们的决策 (Concrete Type):
直接依赖 *zap.Logger 具体类型。
- 原则:在 Handler、Service、Repository 层,注入的类型就是
*zap.Logger。 - 测试怎么办:不要 Mock 日志接口。在单元测试中,直接传入
zap.NewNop()(什么都不做)或者zap.NewExample()(输出到测试控制台)。这比 Mock 一个接口要简单且真实得多。
- 原则:在 Handler、Service、Repository 层,注入的类型就是
3. 访问模式:混合单例与依赖注入 (The Hybrid Accessor)
结合之前讨论的 Option A+B,我们通过设计模式来解决“初始化顺序”和“热加载”的问题。
-
设计挑战:
如果 main.go 还没来得及读配置初始化 Logger,其他 init() 函数里就调用了日志,程序会 Panic。
-
我们的决策 (Thread-Safe Proxy):
- 原子替换 (Atomic Swap):全局变量
globalLogger不会直接暴露给外部修改。我们将使用unsafe.Pointer或atomic.Value(配合 Zap 的ReplaceGlobals) 来保证在运行时重新加载配置(如动态修改 Log Level)时,不会发生并发读写冲突。 - 懒汉式兜底 (Lazy Fallback):在
internal/pkg/log的init()中,我们会默认初始化一个Console Logger。这样即使main函数一行代码都没跑,只要引用了包,日志功能就是可用的(虽然配置是默认的)。这极大提升了开发体验(DX)。
- 原子替换 (Atomic Swap):全局变量
4. 字段构建模式:结构化优先 (Field-First API)
这关乎团队的编码规范,属于 API 设计模式。
-
传统误区 (Printf Style):
使用 SugaredLogger 的 Infof("User %s login failed, error: %v", user, err)。
- 后果:日志分析系统(ELK)只能拿到一串文本,无法对
user进行聚合统计。
- 后果:日志分析系统(ELK)只能拿到一串文本,无法对
-
我们的决策 (Structured Style):
默认只暴露 Logger(强类型),在必要时才暴露 SugaredLogger。
- 强制规范:代码中必须写成
log.Info("user login failed", zap.String("user", user), zap.Error(err))。 - 设计意图:通过 API 的设计,“强迫”开发者思考每一个字段的语义。这虽然写起来繁琐一点,但对于后期的运维和排查是无价的。
- 强制规范:代码中必须写成
总结:设计规格书的基调
基于以上讨论,在接下来的规格说明书中,我们将确立以下基调:
- 不造轮子:核心逻辑全权委托给
zap和lumberjack。 - 薄封装:
pkg/log代码行数应控制在 200 行以内,只做配置解析和 Context 桥接。 - 强类型:严禁在核心路径使用
interface{}。 - 显式传递:通过
WithContext显式传递上下文,而不是依赖某些黑魔法(如 Goroutine Local Storage)。