Files
cs-note/hzh/GO/工程模块化.md
T
2026-05-24 11:42:38 +08:00

306 lines
12 KiB
Markdown
Raw 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: [go, engineering, modularization, architecture, project-structure]
create time: 2026-04-29 15:30
---
# Go 工程模块化
## 概述
系统梳理 Go 项目的模块化和目录结构设计,从项目级别的架构模式到包粒度的拆分策略,涵盖工程化实践中最常见的结构选型和陷阱。
## 一、Go 模块与包的基础概念
> [!question] 思考:Module 和 Package 有什么区别?
>
> - **Module(模块)**:`go.mod` 定义的最小版本管理单元,发布、依赖管理的边界。一个 Module 可以包含多个 Packages。
> - **Package(包)**:代码组织和编译的单位,同一目录下所有文件必须属于同一个 Package。import 的路径粒度是 Package 级别。
核心关系:**Module → 多个 Package → 多个 .go 文件**
```go
module github.com/user/my-project // go.mod — 模块标识
// import 路径
github.com/user/my-project/pkg/user // Package: pkg/user
github.com/user/my-project/internal/handler // Package: internal/handler
```
> [!tip] 关键规则
>
> - `internal/` 下的包只能被其父目录及以内的代码导入,外部 module 无法引用。这是 Go 语言层面强制的可见性约束,比 `private` 关键字更可靠。
> - 不要在 `internal/` 里放第三方库或公共工具——它的语义就是"内部使用"。
## 二、项目级结构选型
### 2.1 标准 Go 项目布局(Standard Go Project Layout)
这是社区最广泛认可的项目结构规范,由 [golang-standards/project-layout](https://github.com/golang-standards/project-layout) 提出并维护:
```
my-project/
├── cmd/ # 入口点,每个子目录是一个可执行命令
│ └── myapp/
│ └── main.go # 入口极薄,只做初始化调用
├── internal/ # 私有代码,外部无法 import
│ ├── handler/ # HTTP/gRPC 处理器
│ ├── service/ # 业务逻辑层
│ ├── repository/ # 数据访问层
│ ├── model/ # 领域模型
│ └── config/ # 配置加载
├── pkg/ # 可对外发布的公共包
│ ├── middleware/
│ └── util/
├── api/ # API 定义(proto/openapi/grpc-gateway)
├── web/ # 前端静态资源
├── configs/ # 配置文件示例
├── scripts/ # 构建/部署脚本
├── docs/ # 文档
├── tests/ # 外部集成测试
├── go.mod
├── go.sum
└── README.md
```
> [!success] 适用场景
>
> - 需要开源或向其他团队分发 `pkg/` 中的代码
> - 项目会持续增长,需要明确的组织规范
> - 团队协作规模较大(5+ 人)
### 2.2 整洁架构(Clean Architecture / Onion Architecture)
以**业务核心**为中心,外层依赖内层,所有方向指向内:
```
my-project/
├── cmd/app/main.go
├── internal/
│ ├── domain/ # 【最内层】实体、值对象、领域接口
│ │ ├── entity/ # User, Order 等核心实体
│ │ ├── valueobject/ # Money, EmailAddress 等值对象
│ │ └── repository/ # 领域接口定义(契约)
│ ├── application/ # 用例/应用服务
│ │ └── usecase/ # CreateUserOrder 等业务编排
│ ├── infrastructure/ # 【外一层】实现细节
│ │ ├── persistence/ # 数据库驱动实现
│ │ ├── externalapi/ # 第三方支付、邮件等
│ │ └── auth/ # JWT/OAuth 实现
│ └── interfaces/ # 【最外层】接入方式
│ ├── http/ # HTTP handlers + router 注册
│ ├── grpc/ # gRPC server
│ └── cli/ # CLI 命令行
└── go.mod
```
依赖方向图示:
```mermaid
graph LR
A["interfaces (HTTP/gRPC)"] --> B["application (usecases)"]
B --> C["domain (entities)"]
D["infrastructure (DB/API)"] --> C
D -.->|implements| B
style C fill:#f9f,stroke:#333,stroke-width:3px
style B fill:#bbf,stroke:#333
style D fill:#bfb,stroke:#333
style A fill:#fbf,stroke:#333
```
> [!info] Clean Architecture 核心原则
>
> 1. **依赖倒置**:内层定义接口,外层提供实现。这样换数据库、换框架不需要改动业务逻辑。
> 2. **域独立**:`domain/entity` 不依赖任何框架或外部库,就是纯 struct。
> 3. **切换成本低**:MySQL → PostgreSQL 只改 `infrastructure/persistence` 一个目录。
> [!warning] 何时不用
>
> 简单 CRUD 项目引入 Clean Architecture 会带来过度抽象。按以下标准判断:
>
> | 条件 | 推荐方案 |
> |------|----------|
> | 单体、单一数据库、快速迭代 | 分层简单结构(见下方) |
> | 多团队、多数据源、长周期 | Clean Architecture |
> | 领域复杂(金融、电商交易) | Clean Architecture 或 DDD |
> | MVP / PoC | 越简单越好 |
### 2.3 轻量分层结构(适合大多数中小型项目)
如果 Clean Architecture 太笨重,三层是最小可用划分:
```
my-project/
├── cmd/app/main.go
├── internal/
│ ├── handler/ # 接收请求、参数校验、调 service
│ ├── service/ # 业务逻辑、流程编排
│ └── repo/ # SQL 查询、缓存读写
├── pkg/ # 可复用组件
└── go.mod
```
**黄金法则**:请求流向是单向的 `handler → service → repo`,禁止反向调用。
## 三、包粒度与拆分策略
### 3.1 拆到什么粒度合适?
> [!question] 包的合理大小是多少?
>
> 没有一个精确的数字,但有以下参考基准:
| 维度 | 建议范围 | 说明 |
|------|----------|------|
| 文件数 | 3 ~ 15 个 | 少于 3 可能该合并;超过 15 需检查是否职责过多 |
| LOC | 200 ~ 1500 行 | 纯接口/类型声明可适当放宽 |
| 职责 | 围绕一个概念 | `user`、`order`、`payment`——用领域名词命名 |
> [!failure] 常见反模式
>
> - **utils 包地狱**:把所有工具函数扔进 `pkg/utils`,包越来越大、越来越难找东西。
> - **解法**:按功能域拆分——`stringutil`、`datetimeutil`、`httputil`。或者直接在使用的地方新建包。
> - **一个包对应一个文件**:如果文件之间强相关,应该放在同一个包里,避免跨包循环引用。
> - **过深的包路径**:`internal/a/b/c/d/e` 超过 4 层通常意味着分类有问题。
### 3.2 按什么维度拆包?
三种主流维度,实际项目中常组合使用:
```mermaid
flowchart TD
A["包拆分维度"] --> B["按职责分层<br/>handler/service/repo"]
A --> C["按业务域分组<br/>user/order/payment"]
A --> D["混合模式"]
D --> E["internal/user/handler<br/>internal/user/service<br/>internal/user/repo"]
style E fill:#f9f,stroke:#333,stroke-width:3px
```
| 模式 | 结构示意 | 适合 |
|------|----------|------|
| **分层式** | `handler/`, `service/`, `repo/` | 单一聚合根、领域不太复杂 |
| **域驱动式** | `user/`, `order/`, `payment/` | 多领域边界清晰、大团队 |
| **混合式** | `user/handler`, `user/service` | **多数项目的最佳平衡点** |
### 3.3 共享包 vs internal 包的决策树
```
某个包要不要暴露出去?
├── 其他 module 会需要吗?
│ ├── 会 → pkg/xxx
│ └── 不会 → internal/xxx
└── 整个项目都不需要 import 呢?
└── 直接放在所属业务包下,不要单独建包
```
> [!tip] `pkg/` 的使用建议
>
> - 放进 `pkg/` 等于承诺这是一个稳定的公开 API——其他人会依赖它。
> - 常见的放 `pkg/` 的内容:日志封装、数据库连接池初始化、通用中间件、配置结构体。
> - 如果不打算开源且没有外部消费者,**优先放 `internal/`**。
## 四、避免循环依赖
Go 编译器在编译期就会拒绝循环 import,所以不会像 Java 那样出现运行时问题。但在编码阶段频繁遇到,以下是高频场景和解法:
### 4.1 常见循环依赖路径
```
handler → service → repository → model → handler ❌
```
> [!failure] 根因:model 定义了需要在 handler 中序列化的字段(如 `LastLoginTime`),repository 也用到 model。
### 4.2 解法矩阵
```mermaid
quadrantChart
title 循环依赖解法选择
x-axis "低复杂度" --> "高复杂度"
y-axis "侵入性小" --> "侵入性大"
"提取接口": [0.2, 0.15]
"下沉共享包": [0.4, 0.35]
"引入 DTO": [0.5, 0.55]
"依赖注入": [0.7, 0.75]
```
| 解法 | 做法 | 适用场景 |
|------|------|----------|
| **接口下沉** | 把接口移到更通用的包(如 `internal/repository/contract`) | service 和 repo 之间的依赖混乱 |
| **共享模型包** | 创建 `internal/model` 存放双方共用的类型 | model 确实被多层共用 |
| **DTO 隔离** | handler 和 domain 用不同的类型,通过转换函数对齐 | 序列化格式和领域模型不一致 |
| **依赖注入** | 用构造器传入依赖而非全局变量或直接 import(详见 [[依赖注入]]) | 大型项目、测试友好 |
## 五、Go 项目的目录设计 Checklist
初始化新项目时按此顺序思考:
```mermaid
flowchart LR
A["确定项目性质"] --> B{"需要对外发布包?"}
B -->|是| C["使用标准布局 + pkg/"]
B -->|否| D["精简布局,全放 internal/"]
D --> E{"领域复杂度?"}
E -->|简单 CRUD| F["轻量三层结构"]
E -->|复杂业务| G["Clean Architecture"]
F --> H["按单一路径拆分: handler/service/repo"]
G --> I["按域分组 + 分层"]
C --> J["开始编码"]
H --> J
I --> J
```
**逐项检查清单**:
- [ ] `go.mod` 的 module name 已定好(建议用完整 import path,如 `github.com/org/repo`)
- [ ] `cmd/` 下有 `main.go`,且逻辑不超过 20 行(只做依赖组装和应用启动)
- [ ] `internal/` 包含了所有一级私有包
- [ ] 每个包的 `package` 名和目录名一致(Go 强制要求,但容易在移动目录时遗忘)
- [ ] 没有 `utils`、`common`、`tools` 这类万金油包名
- [ ] 跨包引用是单向的,无环(可用 `goimports -l` 辅助检查)
- [ ] 测试文件跟源文件放在一起,同包测试用 `_test.go`,白盒外部测试用 `x_test.go`(`package xxx_test`)
## 六、模块化管理实战要点
### 6.1 单 Module vs 多 Module
```mermaid
graph LR
A["项目依赖关系"] --> B{"各部分版本号需要独立控制?"}
B -->|是| C["Monorepo + 多 go.mod"]
B -->|否| D["单个 go.mod"]
C --> E["各自独立版本发布"]
D --> F["统一版本,最简单"]
style D fill:#bfb,stroke:#333,stroke-width:3px
style C fill:#fbf,stroke:#333
```
> [!summary] 决策建议
>
> - **绝大多数项目**:一个 `go.mod` 就够了。Go 的包系统已经足够做模块化解耦。
> - **需要多 Module 的场景**:多个团队独立发布、不同生命周期(一个高频发版一个低频)、或作为独立 library 提供给他人。
> - Monorepo 多 Module 时,用 `replace` 指令在本地开发时指向 sibling module,CI 时移除。
### 6.2 Import 路径风格约定
```
✅ 推荐
internal/user/service
internal/user/service/user.go // 文件名不必与包名重复
❌ 应避免
internal/user-service/service // 连字符不符合 Go 命名惯例
internal/user_service/ // 下划线也不常用
pkg/util/helper.go // 嵌套过深,helper 本身没有信息量
```
**基本原则**:import 路径应该是可读的英文短语,用 `/` 分隔单词,全小写。
## 关联笔记
- [[测试目录结构]]
- [[依赖注入]]