--- 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["按职责分层
handler/service/repo"] A --> C["按业务域分组
user/order/payment"] A --> D["混合模式"] D --> E["internal/user/handler
internal/user/service
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 路径应该是可读的英文短语,用 `/` 分隔单词,全小写。 ## 关联笔记 - [[测试目录结构]] - [[依赖注入]]