go项目结构应按业务域而非技术分层组织,用/internal/和/pkg/划清可见性边界,cmd/仅做初始化,模块化优先用replace模拟,新增包须立即验证依赖。

Go 项目结构设计不是选“好看目录”的问题,而是决定你半年后敢不敢动某段代码、新成员三天内能不能上手、CI 构建失败时能不能快速定位模块边界——结构错了,重构成本会指数级上升。
按业务域组织包,而不是按技术分层
看到 controllers/、services/、repositories/ 这种顶层目录,基本可以判断项目正在积累技术债。这类结构让不同业务的 handler、service、repo 全混在一个目录里,改一个用户登录逻辑,得翻遍三个目录找关联文件。
- 正确做法是每个业务功能自成一包:
/user、/order、/payment,包内再按职责拆文件(如user/service.go、user/repo.go) -
/user包对外只暴露UserService接口和CreateUserRequest类型,不导出数据库模型或中间件实现 - 跨业务调用必须走接口,禁止
import "xxx/internal/order/repo"这类直连内部实现的写法 - 如果某个包开始导入超过 3 个其他业务包,说明它已承担过多协调职责,该拆或重定义边界
用 internal/ 和 pkg/ 划清可见性边界
Go 的首字母大小写导出规则只能控制符号级可见性,internal/ 是 Go 工具链强制的包级隔离机制——任何路径含 /internal/ 的包,都无法被本项目以外的 module 导入。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
-
internal/放纯私有逻辑:比如internal/config、internal/middleware、internal/server,它们可被cmd/api和cmd/worker共用,但绝不允许外部项目 import -
pkg/放真正可复用的公共能力:比如pkg/util(通用校验)、pkg/httpx(封装 http.Client)、pkg/uuid(统一 ID 生成),这些包要有完整单元测试和文档注释 - 别把日志封装塞进
pkg/common这种模糊命名里——pkg/logger才能让人一眼明白用途和契约 - 一旦发现
pkg/下某个包被 3 个以上业务模块强依赖,就要警惕它是否在演变成“上帝工具包”,考虑按场景进一步拆分
主入口只做初始化,不掺业务逻辑
cmd/ 目录存在的唯一意义,是让 main.go 成为薄薄一层胶水。只要你在 main.go 里看到业务判断、数据转换、HTTP 路由注册以外的逻辑,结构就已失衡。
-
cmd/api/main.go只做四件事:解析 flag、加载internal/config、构建internal/server实例、调用.Run() - 所有中间件注册、路由绑定、健康检查端点,都下沉到
internal/server包里,通过函数选项(functional option)注入依赖 - 需要多个二进制?不要复制
main.go,而是新建cmd/worker/main.go,复用同一套internal/逻辑,只替换启动参数和入口函数 - 如果
cmd/下某个main.go超过 50 行,说明初始化逻辑已泄漏,该抽离成独立初始化函数
模块化演进要靠 replace,而不是硬拆仓库
很多团队一上来就想把 /user 拆成独立 Git 仓库 + 单独 CI,结果发现版本对齐困难、本地调试断链、PR 合并阻塞——模块化不是目的,解耦才是。
- 初期用
go.mod中的replace模拟模块边界:replace example.com/project/user => ./internal/domain/user,这样其他模块能import "example.com/project/user",且支持独立go test - 只有当
/user包需被 2 个以上无关项目引用,或由不同团队维护时,才迁出为真实 Git 仓库,并打v1.2.0语义化 tag - 所有模块共用一个
tools.go管理开发工具依赖(golangci-lint、swag),避免go install全局污染和版本不一致 - 别忽略
go list -m all输出里的 indirect 依赖——它们往往是模块边界模糊的信号,比如某个pkg/util悄悄引入了数据库驱动
最常被跳过的动作,是在每次新增包时立刻写 go test -v ./... && go vet ./... ——不是为了跑通,而是验证包之间没有意外的循环导入或隐式依赖。结构设计的成败,往往藏在第一次 go build 成功之后的那三分钟里。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










