go项目目录结构需严格遵循工具链规范:main.go必须置于cmd//下;internal/是语法级访问限制,仅放本项目私有代码;pkg/代表对外承诺的稳定api;测试文件须与源码同目录同包;go.mod的module名必须与仓库地址完全一致。

Go 项目目录结构没有编译器强制要求,但 go build、go test、go list 等工具链行为严重依赖路径与包名的一致性。乱放 main.go 或混用 internal/ 和 pkg/ 会直接导致构建失败、测试不可达、CI 报错,甚至让别人 go get 时导入路径失效。
main.go 必须在 cmd/ 子目录下,不能裸露在根目录
根目录放 main.go 是新手最常踩的坑。一旦加了第二个 package main 文件(比如临时加个 migrate.go),go build ./ 就会报 multiple main packages;更隐蔽的是,go run . 可能静默失败,而 go list ./ 会漏掉某些包,导致 CI 中依赖分析出错。
- 正确做法:每个可执行程序独占一个
cmd/<name>/</name>目录,里面只放main.go(和极少量 CLI 参数解析逻辑) - 例如:
cmd/api/main.go构建出二进制api,cmd/cli/main.go构建出cli - 运行命令统一为
go run cmd/api/main.go,构建命令为go build -o bin/api cmd/api - 别在
cmd/<name>/</name>下再建子目录——那会创建新包,破坏main的唯一性
internal/ 是硬性访问边界,不是代码回收站
internal/ 不是风格建议,是 Go 工具链的语法级限制:任何包路径含 /internal/ 的模块,外部项目 import 会直接编译失败,报 use of internal package not allowed。滥用它会导致重构困难或意外暴露实现细节。
- 只放真正不承诺 API 稳定性、且仅本项目内使用的代码,比如
internal/dbconn(封装连接池初始化)、internal/handler(HTTP 路由绑定)、internal/model(领域结构体) - 禁止把通用工具函数塞进
internal/utils——该归pkg/utils或拆成独立模块 -
internal/下的子目录名应体现高内聚功能(如internal/trade),而非技术角色(如internal/service) - 如果
go.mod的module名写错(比如写成myapp而非github.com/user/myapp),internal/规则会彻底失效
pkg/ 代表对外承诺,建之前要想清楚兼容成本
pkg/ 下的包会被外部项目 go get 直接引用,意味着你必须对导出符号的签名、行为、错误类型做向后兼容保证。随便往里塞代码,等于提前给自己埋下版本管理雷。
- 只放明确设计为复用、有稳定接口、文档完备的组件,比如
pkg/logger(封装 zerolog 并暴露Logger接口)、pkg/httpclient(带默认超时和 metrics 的 client 工厂) - 数据库 model、配置解析器这类强绑定项目上下文的代码,不该进
pkg/——它们属于internal/ - 没想清楚要不要被别人 import?先别建
pkg/。宁可先全放internal/,等真有外部调用需求再迁移 -
pkg/里的函数改参数或删方法,就得升大版本号,否则会破坏下游构建
测试文件必须和源码同目录同包,_test.go 后缀是硬规则
Go 的测试机制不支持集中式 test/ 目录。把 user.go 和 user_test.go 分开放,go test 就找不到测试,也无法访问未导出字段或函数——这是很多开发者调试半天才发现的“失联”问题。
- 单元测试文件必须和被测源码在同一目录,包声明写成
package user_test(不是user),才能测私有逻辑 - 跨包复用的测试辅助代码(比如 mock 工具、测试 HTTP client)放进
internal/testutil,别放pkg/——避免把测试细节暴露给外部 - 集成测试(启动真实 DB、调第三方 API)建议单独建
integration/目录,并用go test -tags=integration控制开关,防止本地开发或 CI 被拖慢 -
go test ./会递归扫描所有目录,但只运行符合*_test.go+ 同包规则的测试
最易被忽略的其实是 go.mod 的 module 名——它不是项目名,而是未来所有人 import 你的包时写的路径前缀。写错一个字符,internal/ 隔离就形同虚设,本地开发 import 会冲突,go proxy 也拉不到版本。这个值必须和 GitHub/GitLab 地址完全一致,且从一开始就要定死。











