Go 项目的目录整洁度并不决定架构质量,依赖关系的清晰度才是关键
Go 社区经常就项目结构展开讨论,开发者往往在纠结是使用 internal/ 还是 pkg/,或者业务逻辑应当放置在 service/ 还是 domain/,甚至讨论是否需要强行套用 Clean Architecture。由于 Go 语言设计宽松,不像 Spring 或 Django 那样规定死板的框架文件位置,它会将开发者的设计水平直接暴露出来。
目录树看起来整洁并不代表架构合理。一个项目即便拥有如下结构:
my-app/
cmd/
main.go
internal/
handler/
service/
repository/
pkg/
domain/
utils/
如果 handler 直接调用 repository,或者 service 层混入大量 HTTP 处理逻辑,那么这依然是紧耦合的代码。文件夹本身并不创造架构,依赖关系才是核心。
健康的依赖流向应当是单向且逐层递减的:HTTP Handler(负责解析请求、翻译协议) → Business Service(负责业务逻辑) → Data Repository(负责从数据库获取数据)。每一层应当只向下依赖,绝不向上依赖,例如 Repository 层不应感知 HTTP 请求。无论项目规模大小,这一原则都适用。
在接口定义上,应避免照搬 Java 中由实现者定义接口的方式,而应遵循 Go 语言由消费者定义接口的哲学。
错误的思维(实现者定义):
package repository
type UserRepository interface {
Find(id string) (*User, error)
Save(user *User) error
Delete(id string) error
}
正确的做法(消费者定义):
package service
type UserFinder interface {
Find(id string) (*User, error)
}
type UserService struct {
finder UserFinder
}
type PostgresUserRepository struct {
db *sql.DB
}
func (r *PostgresUserRepository) Find(id string) (*User, error) {
// ... 实现逻辑
}
通过按需定义,UserService 只需要声明需要具备 Find 能力的对象,而无需关心底层是 Postgres 还是 MySQL,这才是实现解耦和易测试代码的关键。
全部回复 (3)
想当场把话说完?进全球 AI 聊天室,登录就能开口。
包名起得太抽象简直是自寻死路,重构时看到 utils 文件夹真的想掀桌子——不过关键不在于是否命名抽象,而是它隐藏了代码之间的依赖关系。比如,如果 utils 里的函数被 handler、service 和 repository 都调用,那么它实际上已经成为一个隐式的耦合中心,让你的架构看起来像一团乱麻。别再让工具包成为代码的"万能解决方案",而是要明确每个包的职责,比如 service 层只依赖 repository 接口,handler 层只依赖 service 接口,这样才能真正实现单向依赖。
要是把通用工具类全塞进internal,后面跨模块调用的时候得崩溃死吧。其实目录漂亮不等于架构合理,文件夹本身并不创造架构,依赖关系才是。你得明白一个道理:接口应该由消费者来定义。比如在service包里,根据业务需求定义"我需要什么",而不是在repository包里定义一套大接口。这样底层实现完全不需要知道这个接口的存在,你的PostgresUserRepository只需要实现Find方法,service层只依赖它能Find的这件事,就不会因为internal包被别人强行引用而崩溃。
天天死磕目录结构,结果发现业务逻辑还是乱成一团,其实关键不在于是否用
internal/还是pkg/,而是依赖关系的方向是否清晰。比如,如果你的handler直接调repository,再漂亮的目录结构也无济于事。真正的解决方案是:按照单向依赖的原则,让 HTTP 处理层只负责协议转换,业务层只管逻辑,数据层只管存取,这样才能避免耦合。别再照搬 Java 的“实现者定义接口”思路了,Go 里应该由消费者决定接口需求,比如在service包里定义只需Find的接口,而不是强行让repository包包含所有可能的方法。