go微服务模块拆分必须以业务域为边界,采用一服务一模块、包内职责分离、接口与实现隔离、internal/pkg可见性管控等实践。

模块拆分必须以业务域为边界,不是按技术层
把 handlers、services、repositories 拉平放在项目根目录下,是 Go 微服务项目半年后难以维护的起点。这种结构让所有业务逻辑挤在同一个命名空间里,改个订单状态流转,得横跨三个目录翻代码,还容易误改其他业务的实现。
正确做法是每个业务功能自成一个包:/user、/order、/payment,包内再按职责组织文件(如 user/service.go、user/repo.go)。对外只暴露接口和 DTO 类型,比如 user.UserService 和 user.CreateUserRequest,绝不导出数据库模型或中间件细节。
- 跨业务调用必须走接口,禁止直接 import
"xxx/internal/order/repo"这类路径 - 如果某个包 import 超过 3 个其他业务包,说明它已变成协调中心,该拆或重划边界
-
/user包内部可以自由使用user.NewRepo(),但外部只能通过user.NewService(repo user.Repo)注入依赖
go.mod 是模块边界的唯一权威,别信目录名
Go 不靠目录名识别模块,靠的是 go.mod 中声明的 module path。你可以在 github.com/yourorg/foo 下建任意嵌套目录,只要 go.mod 写着 module github.com/yourorg/foo,它就是一个独立模块——能单独打 tag、发版本、被其他项目 go get。
常见错误是把多个服务塞进一个 monorepo 且共用一个 go.mod,结果改支付服务时不小心升级了用户服务的依赖版本,CI 直接挂掉。2026 年主流团队已普遍采用「一服务一模块」:每个服务有自己 go.mod,主应用通过 replace 或私有 proxy 管理本地开发联调。
- 新建服务时第一件事就是执行
go mod init github.com/yourorg/payment - 模块路径必须全局唯一,避免用
foo-api这类拼接名,用语义化路径如github.com/yourorg/payment/api - 本地开发联调时,在主应用的
go.mod里加replace github.com/yourorg/payment => ../payment
internal/ 和 pkg/ 的可见性规则不能靠约定来守
Go 的首字母大小写只控制符号导出,internal/ 才是工具链强制的包级隔离机制:任何含 /internal/ 的路径,都无法被本模块以外的代码 import。这不是建议,是编译器报错级别的约束。
把配置加载、中间件、HTTP 封装塞进 internal/config、internal/middleware、internal/server,它们可被 cmd/api 和 cmd/worker 共用,但绝不会被外部项目引用。而真正可复用的能力(如通用 ID 生成、结构化日志封装)必须放 pkg/ 下,且要有完整测试和文档注释。
-
pkg/logger比pkg/common强十倍——名字即契约,别人一眼知道它干啥、能不能引入 - 一旦发现
pkg/下某个包被 3 个以上业务模块强依赖,就要警惕它正在演变成“上帝包”,考虑按场景拆(比如pkg/uuid和pkg/snowflake分开) -
cmd/api/main.go只做四件事:解析 flag、加载internal/config、构建internal/server实例、调用.Run()—— 多一行业务逻辑都是结构失衡
接口与实现分离必须靠包隔离,不是靠文件名
把 FooService 接口和 fooServiceImpl 放同一个包里,等于把鸭子和鸭子的毛线团锁进同一个抽屉。单元测试时无法注入 mock,依赖注入容器也无从下手,更别说替换实现(比如用内存版 repo 做集成测试)。
标准做法是在服务模块内划分语义化子包:api 定义接口和请求响应类型,impl 提供默认实现,mock 存桩实现,client 封装调用方逻辑。主应用通过导入别名明确区分:
import (
uapi "github.com/yourorg/user/api"
uimpl "github.com/yourorg/user/impl"
papi "github.com/yourorg/payment/api"
pimpl "github.com/yourorg/payment/impl"
)
func main() {
var userSvc uapi.UserService = uimpl.NewUserService()
var paySvc papi.PaymentService = pimpl.NewPaymentService()
}
- 包名用
impl而非implementation:Go 社区惯例,简短、小写、无冗余 - 绝对禁止在
api包里 import 具体实现(如database/sql),否则接口层就被污染了 - 如果
api包开始出现func NewXXX()工厂函数,说明它已悄悄承担了创建职责,该重构
User 结构体,或者统一错误码。这些看似省事的做法,实际会把两个服务的生命周期绑死。2026 年一线团队的共识是:宁可重复定义,也要保持物理隔离。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











