81 lines
4.5 KiB
Markdown
81 lines
4.5 KiB
Markdown
---
|
||
tags: []
|
||
aliases:
|
||
- 1. 核心模式:装饰器模式的变体 (Context-Decorator Pattern)
|
||
date created: 星期三, 十二月 10日 2025, 10:37:54 晚上
|
||
date modified: 星期三, 十二月 10日 2025, 10:38:26 晚上
|
||
---
|
||
|
||
# 1. 核心模式:装饰器模式的变体 (Context-Decorator Pattern)
|
||
|
||
这是我们处理 `TraceID` 和上下文的核心手段。
|
||
|
||
- 传统误区 (Over-Abstraction):
|
||
|
||
定义一个庞大的 MyLogger 结构体,把 zap.Logger 藏在里面,然后重写 Info, Error 等所有方法。
|
||
|
||
- _后果_:维护成本极高,每次 Zap 更新或增加新特性(如 `Panic` 或 `DPanic`),你都得跟着改代码。且容易在转发参数时产生逃逸分析(Escape Analysis)导致的内存分配。
|
||
- 我们的决策 (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 一个接口要简单且真实得多。
|
||
|
||
# 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)。
|
||
|
||
# 4. 字段构建模式:结构化优先 (Field-First API)
|
||
|
||
这关乎团队的编码规范,属于 API 设计模式。
|
||
|
||
- 传统误区 (Printf Style):
|
||
|
||
使用 SugaredLogger 的 Infof("User %s login failed, error: %v", user, err)。
|
||
|
||
- _后果_:日志分析系统(ELK)只能拿到一串文本,无法对 `user` 进行聚合统计。
|
||
- 我们的决策 (Structured Style):
|
||
|
||
默认只暴露 Logger(强类型),在必要时才暴露 SugaredLogger。
|
||
|
||
- _强制规范_:代码中必须写成 `log.Info("user login failed", zap.String("user", user), zap.Error(err))`。
|
||
- _设计意图_:通过 API 的设计,“强迫”开发者思考每一个字段的语义。这虽然写起来繁琐一点,但对于后期的运维和排查是无价的。
|
||
|
||
---
|
||
|
||
# 总结:设计规格书的基调
|
||
|
||
基于以上讨论,在接下来的规格说明书中,我们将确立以下基调:
|
||
|
||
1. **不造轮子**:核心逻辑全权委托给 `zap` 和 `lumberjack`。
|
||
2. **薄封装**:`pkg/log` 代码行数应控制在 200 行以内,只做配置解析和 Context 桥接。
|
||
3. **强类型**:严禁在核心路径使用 `interface{}`。
|
||
4. **显式传递**:通过 `WithContext` 显式传递上下文,而不是依赖某些黑魔法(如 Goroutine Local Storage)。
|