Files
Inbox/Go项目实战/03_基础设施/02_日志/03_核心设计模式.md
2025-12-11 07:24:36 +08:00

81 lines
4.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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