别再纠结 Go 的目录结构了,代码写烂了真不是语言的锅
internal/ 还是 pkg/?业务逻辑该塞进 service/ 还是直接放 domain/?是不是一定要强行套用 Clean Architecture?说实话,这种争论挺没意义的。Go 这种语言本身就是极其“宽松”的,它不像 Spring 或者 Django 那样给你规定好一套死板的框架,告诉你这个文件必须放哪。但这不代表 Go 没法写出高质量的代码,真相是:Go 不会主动诱导你写出烂架构,它只是把你的设计水平直接暴露了出来。
文件夹整齐不代表架构合理
我见过很多 GitHub 仓库,目录树看起来极其漂亮,简直是强迫症的福音:
my-app/
cmd/
main.go
internal/
handler/
service/
repository/
pkg/
domain/
utils/看起来很专业对吧?但如果你点进去看代码,可能发现 handler 直接去调 repository,或者 service 层里混杂了大量的 HTTP 处理逻辑。这种项目本质上就是一堆紧耦合的垃圾,只是披了一层整洁的目录外衣。
你要明白一个核心点:文件夹并不能创造架构,依赖关系(Dependencies)才是。
架构的核心是依赖的方向
真正的架构设计,其实是在做关于“代码如何依赖其他代码”的决策。一个健康的流向应该是单向且递减的:
- HTTP Handler (只负责解析请求、翻译协议)
- ↓
- Business Service (只负责跑业务逻辑)
- ↓
- Data Repository (只负责从数据库拿数据)
这种流向的价值在于每一层都只向下依赖,绝不向上依赖。如果你把这个顺序反过来,让 Repository 层去感知 HTTP 请求,那你的代码离崩溃也就不远了。无论你的项目是只有三个文件的微型工具,还是有五十个包的大型单体,这个原则是通用的。
别再像写 Java 那样定义接口了
这是很多从 Java 转过来的开发者最容易踩的坑。在 Java 里,习惯的做法是“实现者定义接口”,比如在 repository 包里定义 UserRepository 接口,然后写个 PostgresUserRepository 去实现它。
但在 Go 的哲学里,接口应该由消费者(Consumer)来定义。
这是我个人的实战经验,对比一下你就明白了:
错误的思维(实现者定义):
// 在 repository 包里定义,这会让调用方被迫依赖整个大接口
package repository
type UserRepository interface {
Find(id string) (*User, error)
Save(user *User) error
Delete(id string) error
}正确的做法(消费者定义):
// 在 service 包里,根据业务需求定义“我需要什么”
package service
type UserFinder interface {
Find(id string) (*User, error)
}
type UserService struct {
finder UserFinder // 我只需要能 Find 的东西,其他的我不关心
}
// 底层实现完全不需要知道这个接口的存在
type PostgresUserRepository struct {
db *sql.DB
}
func (r *PostgresUserRepository) Find(id string) (*User, error) {
// ... 实现逻辑
}通过这种方式,UserService 并不需要关心底层是 Postgres 还是 MySQL,它只需要声明:“我需要一个具备 Find 能力的东西就行了”。这种“按需定义”的能力,才是 Go 能够写出解耦、易测试代码的关键。别再去纠结你的目录叫 utils 还是 pkg 了,先把依赖关系理顺。