根模块仅作版本锚点,不放业务代码;其go.mod只声明module、go版本、require伪版本及replace本地路径,禁止import子模块或放main.go,否则破坏模块边界导致CI失败。

根模块只做版本锚点,不放业务代码
根目录的 go.mod 不是“主模块”,它唯一作用是统一管理子模块版本和开发时的本地替换。如果在里面写业务逻辑、import 子模块包、甚至放 main.go,会立刻破坏模块边界,导致 CI 构建失败或本地依赖失效。
- 根
go.mod中只保留module声明、go版本、require(指向各子模块的伪版本如v0.0.0)和replace(指向./service/user这类本地路径) - 禁止在根目录下创建
pkg/或internal/—— 它们属于具体子模块的私有范围 - CI 构建时必须先删掉所有
replace行,再go mod tidy,否则发布的 tag 会绑定本地路径而非真实版本
子模块间禁止直接 import internal/ 包
Go 的 internal/ 是编译器强制的访问控制机制:只要 import 路径含 /internal/,就只能被同一 module 下的代码引用。跨模块 import service/user/internal/handler 会直接报错 use of internal package not allowed,但很多人误以为只是“约定”,其实它是硬约束。
- 对外暴露必须通过顶层包(如
service/user)导出 interface 或 struct,且该包不能依赖自身internal/以外的任何子模块实现 - 共享类型(如
User)应放在shared/domain这类独立 module 中,用require引入,而非复制粘贴或跨模块 import - 若两个子模块需协作(如
service/order调用service/user),调用方只依赖service/user的 public 接口,被调用方在internal/中实现,中间用 wire 或手动注入解耦
权限划分靠目录结构 + replace + CI 检查三重锁定
大型团队里,“谁可以改哪个模块”不能靠口头约定或文档,而要由工程结构本身 enforce。关键不是加权限系统,而是让违规操作在本地就失败、在 CI 就卡住。
- 每个子模块(如
api/auth、service/payment)必须有自己独立的go.mod,module path 必须全局唯一(如example.com/project/api/auth) - 开发时用
replace绑定本地路径,但 CI 流水线中运行go list -m all | grep replace,命中即 fail —— 确保发布前无本地路径残留 - 在 pre-commit hook 或 CI 中执行
go list -f '{{.Dir}}' ./... | grep internal,检查是否有人把internal/放在根目录或跨模块路径下
发布流程必须按模块打 tag,不能全量 push
多 module 项目最常踩的坑是:一个 PR 同时改了 service/user 和 api/v1,结果只给 api/v1 打了 v1.2.0 tag,却忘了 service/user 的变更也需要独立版本。下游服务拉取时,go mod tidy 会锁死旧版,导致运行时 panic。
立即学习“go语言免费学习笔记(深入)”;
- 每个子模块的 tag 必须与其
go.mod中声明的 module path 完全匹配(如github.com/org/project/service/user→ tagservice/user/v2.1.0) - Goreleaser 配置必须按子目录拆分,
.goreleaser.yml中用builds列表指定dir: ./service/user,而不是整个仓库一把梭 - 发布后立即在根目录运行
go get -u example.com/project/service/user@v2.1.0并go mod tidy,确保其他模块能感知到新版本
replace 和 internal/ 的理解偏差上——前者是临时开发手段,后者是永久访问限制。不把这两条当铁律,再多的流程规范也挡不住循环依赖和越权引用。


















