Go项目结构受编译器和工具链硬约束:internal/是编译器级防火墙,仅限本module内导入;cmd/下main.go须精简为胶水层;测试文件必须与源码同目录同包名;pkg/是契约交付区,需维护兼容性与文档。

go 项目结构不是“写得整齐就好”,而是直接决定 go build 能不能过、go test 能不能自动跑、别人 import 你的包会不会编译报错——规范背后全是编译器和工具链的硬性约束,不是风格偏好。
为什么 internal/ 目录不是“内部代码收纳盒”而是编译器级防火墙
Go 编译器会主动拒绝任何跨 module 导入含 /internal/ 的路径,比如 github.com/user/project/internal/db 只能被 github.com/user/project/... 下的包 import,外部项目一导入就报 use of internal package not allowed。
常见错误现象:
- 把通用工具函数塞进
internal/util/,结果cmd/api和cmd/worker都要重复实现日志初始化 - 想复用
internal/config里的解析逻辑,却在另一个项目里 import 失败,只能复制粘贴
正确做法:
-
internal/只放**真正不该被外部感知的实现细节**:如internal/tradebot/state_machine.go(含未导出字段和私有方法) - 可复用能力必须挪到
pkg/:比如pkg/config提供LoadYAML接口,带完整测试和文档注释 - 一旦发现某个
internal/xxx被本项目内 3 个以上 cmd 或业务包 import,说明它其实该升为pkg/xxx
cmd/ 目录下 main.go 为什么只能有四行有效代码
cmd/api/main.go 的唯一职责是启动胶水层:解析 flag → 加载 config → 构建 server 实例 → 调用 .Run()。所有业务判断、中间件注册、路由定义都算越界。
立即学习“go语言免费学习笔记(深入)”;
容易踩的坑:
- 在
main.go里直接写http.HandleFunc("/trade", handleTrade),导致无法对 handler 做单元测试 - 把数据库连接池初始化逻辑写在 main 函数里,结果
internal/tradebot包无法独立测试(依赖未注入的全局 db 变量) - 多个命令共存时,误把所有入口写成
cmd/main.go,触发multiple main packages错误
实操建议:
- 每个可执行文件必须独占一个子目录:
cmd/steambot→ 输出二进制名steambot;cmd/worker→ 输出worker -
main.go中禁止出现if、for、switch等控制流(除最外层 flag 解析),只允许调用app.New(...)和.Run() - HTTP 路由注册必须抽到
internal/server包里,用接口接收http.Handler,便于替换 mock
测试文件命名和位置为什么必须和源码严格对齐
Go 不支持集中式 test/ 目录。go test ./ 只扫描当前模块下所有 *_test.go 文件,并且要求它们与同目录非 _test.go 文件属于**同一包名**,否则编译失败。
典型错误:
- 把
internal/tradebot/tradebot.go(package tradebot)的测试写成test/tradebot_test.go(package tradebot_test)→go test根本不识别 - 想测私有方法,却把测试文件放在别的包下,结果访问不到未导出字段,只能暴露一堆
func (t *tradeBot) DoXXXForTest()
关键规则:
- 测试文件必须和源文件同目录:
internal/tradebot/tradebot.go↔internal/tradebot/tradebot_test.go - 若需访问私有成员,测试文件必须声明
package tradebot(和源文件一致) - 若只测导出接口(黑盒),可用
package tradebot_test,但此时无法访问未导出字段或方法 - 别写
func TestMain来做全局 setup —— 它绕过go test的并发控制,容易引发竞态
pkg/ 目录不是“公共函数垃圾桶”,而是契约交付区
pkg/ 是你对外承诺的 API 边界。只要放进 pkg/,就意味着你要维护向后兼容、写文档、加测试、处理 semver 版本升级——它不是“反正别人也能用,先扔这儿再说”的缓存区。
性能与兼容性影响:
-
pkg/logger若返回*log.Logger,下游升级 Go 版本后可能因标准库变更而 break;应封装为接口type Logger interface { Info(...) - 把
pkg/httpx设计成接受context.Context+ 返回error,才能自然融入 Go 生态的超时/取消链路 - 一旦
pkg/uuid被 5 个业务包强依赖,它就不能再随便改Generate()的签名,否则 CI 全红
判断一个包该不该进 pkg/ 的唯一标准:你是否愿意把它发布成独立 module(如 github.com/user/pkg/uuid),并承诺 v1 兼容性? 如果答案是否定的,它就该留在 internal/ 或拆得更细。


















