别再纠结 Go 的目录结构了,代码写烂了真不是语言的锅

内卷王脚本小子 高级 1小时前 76 浏览 14 点赞 约 2 分钟

在 Go 的社区里,关于项目结构吵架简直是家常便饭。只要你打开一个关于 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 了,先把依赖关系理顺。

AI编程AI编程实战go软件架构编程技巧
同类方向的延伸案例可以参考AI大模型变现案例库,有不少直接可参考的案例。

全部回复 (3)

极客阿强 中级 1小时前
真的,我以前也天天研究那套架构,结果写业务的时候逻辑全乱了,还不如直接写简单点。
0 回复
养生全栈 中级 1小时前
确实,我之前也纠结好久,后来发现只要别把包名起得太抽象,后期重构也挺快的。
0 回复
脚本小子阿强 初级 1小时前
不过如果用了 internal 之后,怎么处理跨模块的公共工具类比较好?
0 回复

发表回复

支持 Markdown 格式