go项目模块拆分核心是提升可维护性:internal/包仅限本模块导入,pkg/包供外部复用;handler-service-repository分层保障测试解耦;replace仅用于临时调试,须及时移除。

Go 项目里代码不拆分,不是“写得快”,是“改得慢、测不动、不敢动”。模块拆分不是为了看起来整齐,而是让 go test 能精准跑某一层,让 go build -o cmd/api/main.go 不带无关逻辑,也让别人接手时能一眼看清“哪块管登录,哪块管发消息”。
为什么 internal/ 和 pkg/ 必须分开
internal/ 下的包(如 internal/handler)只能被本模块导入,pkg/ 下的(如 pkg/util)则可被外部项目 go get。混用会导致:
- 把数据库连接池逻辑放
pkg/,结果被其他项目误用,引发连接泄漏 - 把业务校验规则放
internal/,却在测试中硬写replace去 mock,绕过编译检查 -
go list ./...扫出一堆不该暴露的包,CI 构建失败或依赖污染
正确做法:只把真正通用、无项目强耦合的代码放进 pkg/,比如 pkg/uuid、pkg/errcode;所有带业务语义的(internal/order、internal/payment)全进 internal/。
Handler → Service → Repository 分层不是摆设
这三层不是“为了分而分”,而是为了解耦测试和替换实现。常见错误是:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- Handler 里直接调
db.QueryRow,导致无法用内存 mock 测试 HTTP 路由 - Service 方法接收
*http.Request,把 Web 层细节拖进业务层 - Repository 返回
sql.Rows,迫使 Service 自己做 scan,违反单一职责
应确保:
- Handler 只做参数解析、响应包装、错误转 HTTP 状态码
- Service 接收纯结构体(如
CreateOrderInput),返回纯结构体或 error - Repository 接口定义在
internal/repository,具体实现(如pgrepo)放同目录下子包,用接口隔离驱动差异
go.mod 中 replace 的真实用途与风险
replace 是本地开发调试用的,不是长期解耦方案。它容易掩盖真实依赖问题:
- 你用
replace example.com/pkg => ./pkg,但 CI 环境没这行,构建直接失败 - 本地
replace指向未git commit的修改,协作时别人拉不到一致代码 - 多个
replace形成隐式依赖链,升级某个模块时忘了更新对应 replace 行
建议只在以下场景用:go mod edit -replace 临时验证一个未发布的 pkg/ 变更;确认无误后立刻删掉 replace,走正常 require + 版本号流程。
模块拆分最难的不是目录怎么建,而是每次加新功能时,要问一句:“这个逻辑如果明天被另一个服务复用,我现在的包名、接口定义、错误类型,还站得住脚吗?” —— 这个判断比写代码花的时间多,但省下的 debug 时间更多。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










