中大型go项目架构成败取决于按业务域组织包而非分层目录。应以user/等业务包封装完整逻辑,跨业务调用走接口,用internal/隔离实现,pkg/只放高复用组件,通过go.mod replace渐进模块化,cmd/仅作薄胶水层。

中大型 Go 项目架构设计成败,不取决于用了多少框架或中间件,而在于包组织是否让“改一个用户登录逻辑”这件事,只用打开 user/ 目录下的几个文件——且不会意外牵扯到 order/ 或 payment/ 的实现细节。
按业务域组织目录,别碰 controllers/services/repositories 这套分层
这是最常踩的坑:把所有 handler 塞进 handlers/、所有 service 塞进 services/,结果改个用户密码重置逻辑,得在三个目录里跳来跳去,还容易误改其他业务的同名函数(比如 Validate() 在多个 service 里重复实现)。
正确做法是每个核心业务自成一包:
-
user/包含service.go(业务逻辑)、handler.go(只依赖user.Service接口)、repository.go(只声明接口,不写 SQL)、dto.go(API 输入输出结构体) - 跨业务调用必须走接口,禁止
import "xxx/internal/order/repo"这类直连内部实现的写法 - 如果
user/包开始 import 超过 3 个其他业务包,说明它已变成协调中心,该拆或重划边界
用 internal/ 强制隔离私有实现,别靠命名约定
很多人用 pkg/common 或 utils/ 放一堆“好像哪都能用”的函数,结果越积越多,谁都敢改,谁都怕动——这不是复用,是耦合温床。
internal/ 是 Go 工具链硬性规定的访问控制机制,不是目录名建议:
-
internal/config、internal/logger、internal/middleware可被cmd/api和cmd/worker共用,但外部项目无法 import -
pkg/下只放真正可复用、有完整测试和文档的组件,例如pkg/uuid、pkg/httpx;模糊命名如pkg/common必须拆掉 - 一旦发现
pkg/下某个包被 3 个以上业务模块强依赖,就要警惕它是否在演变成“上帝工具包”
Go Modules 配合 replace 实现渐进式模块化
别一上来就拆 Git 仓库。单体项目初期,用 go.mod 内部模拟模块化更安全、更可控。
在根 go.mod 中写:
module example.com/project replace example.com/project/user => ./internal/domain/user replace example.com/project/order => ./internal/domain/order
这样其他模块可以 import "example.com/project/user",还能独立运行 go test 和 go vet。只有当某模块需独立部署或由多团队维护时,才迁出为真实 Git 仓库并打语义化 tag(如 v1.3.0)。
所有开发工具(golangci-lint、swag)统一收口到 tools.go,避免 go install 污染全局环境。
cmd/ 必须是薄胶水层,main.go 里只允许出现四件事
如果在 cmd/api/main.go 里看到数据转换、权限判断、HTTP 路由注册以外的逻辑,结构已经失衡。
main.go 只做:
- 解析命令行 flag
- 加载
internal/config - 构建
internal/server实例(比如 Gin router + middleware 注册) - 调用
.Run()
任何业务判断、错误码映射、DTO 校验、数据库连接池初始化,都必须下沉到 internal/ 下对应包里。否则,你写的不是入口,是第二个 handler 层。
最容易被忽略的是:重构不是一次动作,而是持续验证的过程。每次新增一个包,立刻跑 go list -f '{{.Deps}}' ./your/new/package 看它是否意外引入了不该依赖的包;每次改 go.mod,先确认 go mod graph | grep your-module 是否出现非预期的依赖环。结构问题从来不会报错,但会在半年后让你不敢动某段代码。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











