--- tags: [] aliases: - 1. 代码组织方式 (Code Organization) date created: 星期三, 十二月 10日 2025, 10:42:21 晚上 date modified: 星期三, 十二月 10日 2025, 11:38:44 晚上 --- # 1. 代码组织方式 (Code Organization) 我们将遵循 **“高内聚、低耦合”** 的原则,将日志模块放置在 `internal/pkg/log` 下。这里是所有日志逻辑的物理家园。 建议的文件结构如下(逻辑分层): - **`log.go` (Facade/Entry Point)**: - 这是对外暴露的统一入口。包含全局单例的定义、初始化函数 (`Init`)、以及最常用的静态方法代理(如 `Info`, `Error`, `WithContext`)。 - **设计意图**: 让其他模块只 import 这一个包就能完成 90% 的工作。 - **`options.go` (Configuration)**: - 定义配置结构体(Level, Filename, MaxSize, MaxAge 等)。 - **设计意图**: 将配置解析逻辑与日志初始化逻辑分离,方便单元测试。 - **`zap.go` (Core Implementation)**: - 负责 `zap.Logger` 的具体构建。包含 Encoder 配置(JSON vs Console)、Writer 配置(Lumberjack 集成)和 Level 动态调整逻辑。 - 这是“脏活累活”集中的地方,屏蔽 Zap 的复杂构建细节。 - **`context.go` (The Bridge)**: - **核心组件**。实现 `TraceID` 的提取逻辑。 - 定义如何从 `context.Context` 中挖掘元数据,并将其转化为 `zap.Field`。 --- # 2. 调用方式与依赖注入 (Invocation & DI) 这里有一个经典的架构冲突:**Singleton(单例) vs Dependency Injection(依赖注入)**。我们的策略是 **“依赖注入为主,单例为辅”**,但在具体使用上有一个极其重要的**反直觉设计**。 ## A. 为什么 Service 层不应保存 Request Logger? 你可能会想在 Service 初始化时注入一个带 Context 的 Logger。 - **错误做法**: `type UserService struct { logger *zap.Logger }`,然后在请求进来时试图把 request-scoped 的 logger 塞进去。 - **架构事实**: 在 Wire 依赖注入中,`Service`、`Repository` 通常是 **单例 (Singleton)** 的(即整个应用生命周期只有一个实例)。 - **结论**: 你**不能**把属于某一次 HTTP 请求的 `TraceID` 注入到单例的 Struct 成员变量中。 ## B. 正确的调用范式 (The Best Practice) Logger 作为**工具能力**被注入,Context 作为**请求参数**被传递。 1. **依赖注入 (Setup Phase)**: - 在 `NewUserUsecase` 时,注入基础的 `*zap.Logger`(不带 TraceID)。 - 这个 Logger 配置好了输出路径、Level 等全局属性。 2. **方法调用 (Runtime Phase)**: - 在具体的方法(如 `Register`)中,使用 `log.WithContext(ctx)` 来“临时”生成一个带有 TraceID 的 Logger 实例。 **示例逻辑流**: - **Struct 定义**: `struct { baseLogger *zap.Logger }` - **方法内部**: `l := log.WithContext(ctx, u.baseLogger)` -> `l.Info("user registered")` - **说明**: 这里的 `WithContext` 是一个纯内存操作(浅拷贝),开销极小,可以放心高频调用。 ## C. 高性能场景:作用域复用 (Scoped Logger) 虽然 `log.WithContext` 是浅拷贝,但在循环或长链路中频繁调用仍会产生大量临时对象,增加 GC 压力。 - **反模式 (Anti-Pattern)**: 在 `for` 循环内部调用 `log.WithContext(ctx)`。 - **最佳实践 (Best Practice)**: **作用域提升**。在函数或循环入口处调用一次 `WithContext`,生成局部变量 `l` (Logger),随后全程复用该变量。 --- # 3. 数据流与 TraceID 传递 (Data Flow) 这是实现“全链路可观测性”的生命线。数据流必须打通以下四个关卡: ## 关卡 1:入口 (Entry - Middleware) - **位置**: `internal/middleware/trace.go` (需新建) 或集成在 `response` 包中。 - **行为**: 当 HTTP 请求到达,生成一个 UUID。 - **动作**: 使用 `c.Set("X-Trace-ID", uuid)` 将其放入 Gin 的上下文存储中。同时,将其放入 HTTP Response **动作**: 1. 调用 `pkg/log.WithTraceID(ctx, uuid)` 将 `UUID` 注入标准 `Context`。 2. 执行 `c.Request = c.Request.WithContext(newCtx)` 将其回写。 3. (可选) 同时放入 Gin 上下文存储和 Response Header 供前端使用。 ## 关卡 2:桥接 (Bridge - Context Adapter) - **位置**: `internal/pkg/log/context.go` - **设计原则**: `pkg/log` **不依赖** `gin`,只识别标准库 `context.Context`。 - **行为**: `log.WithContext(ctx) 调用内部帮助函数 GetTraceID(ctx) 获取 TraceID。` - **前置条件**: 必须依赖上游(Middleware)将 TraceID 提前注入到标准 Context 中。 - **输出**: 返回一个预置了 `zap.String("trace_id", id)` 字段的 Logger。 ## 关卡 3:穿透 (Propagation - Service/Repo) - **行为**: 所有的业务方法签名必须包含 `ctx context.Context` 作为第一个参数。 - **动作**: 严禁在层级调用中丢弃 Context(例如使用 `context.Background()` 替代传入的 ctx),这会导致链路断裂。 ## 关卡 4:异步与后台边界 (Async & Background Boundary) - **高危场景**: 在 Handler 中启动 Goroutine 处理耗时任务。 - **陷阱**: `gin.Context` 是非线程安全的。如果 Goroutine 执行时 HTTP 请求已结束,Gin 会重置该 Context,导致数据竞争或脏读。 - **解决方案**: 必须在主协程中执行 `ctx.Copy()`,将副本传递给 Goroutine。日志模块必须支持处理这种副本 Context。 - **新增场景:后台任务 (Background Tasks)** - **场景**: 定时任务 (Cron)、消息队列消费者 (MQ Consumer)、系统初始化。 - **问题**: 初始 `context.Background()` 不包含 TraceID。 - **动作**: 必须调用 `log.StartBackgroundTrace(ctx)` 进行“播种”。该函数会检测 Context,若无 TraceID 则生成新 ID 并注入,确保链路可追踪。 --- # 4. 关键架构思考:防腐层 (Anti-Corruption Layer) 我们在设计时还需考虑一层“防腐”。 - **问题**: 如果未来我们想给所有的日志加一个字段,比如 `env=prod`,或者想把所有的 `trace_id` 改名为 `traceId`。 - **对策**: 所有的业务代码**严禁**直接手动构建 `zap.String("trace_id", …)`。 - **约束**: 这个字段的 Key 必须定义在 `pkg/log` 的常量中,且只能由 `WithContext` 内部逻辑自动附加。业务开发者只负责传 Context,不负责管 ID 怎么拼写。 --- # 总结 - **代码位置**: `internal/pkg/log`,包含 `log.go` (入口), `zap.go` (实现), `context.go` (桥接)。 - **调用方式**: 注入 Base Logger -> 方法内 `WithContext(ctx)` -> 打印。 - **数据流**: Middleware 生成 -> Gin Context 携带 -> Log Adapter 提取 -> Zap Field 输出。 - **并发安全**: 警惕 Gin Context 在 Goroutine 中的误用,强调 `Copy()` 机制。