不少团队在从 Java 迁移到 Go 时,最先暴露出来的并不是语法问题,而是组织代码的习惯问题。像 xxxService.go、xxxDao.go、xxxController.go 这样的命名,在 Java 项目里很常见,但放到 Go 里往往显得别扭。要判断这种写法是否合适,关键不在“能不能用”,而在它是否符合 Go 的设计哲学、命名约定,以及项目演进后的可维护性。
Go 为什么不鼓励照搬 Java 分层命名
Go 和 Java 的一个根本差异,在于它对“统一风格”的态度更强。很多在其他语言里只是风格建议的内容,在 Go 里会通过工具链、约定和社区实践被迅速拉齐。Go 团队一直强调的是简洁、一致、低负担,这种倾向会直接影响包结构、文件命名和职责划分。
因此,讨论要不要写 userService.go,本质上不是在讨论个人喜好,而是在讨论:你的代码是否还在沿用上一门语言的组织方式,是否增加了 Go 项目本来不需要的层级和命名噪音。
如果一个文件名已经叫 user.go,并且它位于 user 包中,那么再额外附加 Service、Dao、Controller 这类后缀,往往并没有增加多少信息量,反而会把 Go 项目拖回“先分技术层,再找业务”的思路里。
Go 里的命名规则,重点不只是文件名
在 Go 世界中,命名不是孤立的,它和目录、包、文件职责是一起考虑的。
包名怎么定
包名通常要和目录名保持一致,并尽量做到简短、可读、有意义。常见做法是全部小写,避免下划线,也不要故意起成和标准库重名的名字。Go 更希望开发者从包名上就能看出这块代码服务于哪个领域,而不是属于哪一层架构。

文件名怎么定
文件名一般也使用小写。如果是组合词,常见做法是采用下划线分隔,例如 user_store.go、user_api.go。这类命名的重点不是“形式统一”本身,而是让读者快速知道文件围绕什么内容展开,而不是看到一串来自 Java 分层模式的技术后缀。

社区约定比个人习惯更重要
Go 社区已经形成了较稳定的约定,例如测试文件以 _test.go 结尾,很多接口命名会以 er 结尾。既然已经进入 Go 生态,优先遵循这些已形成共识的规则,通常比保留历史语言的编码习惯更有价值。

为什么不推荐 `Service`、`Dao`、`Controller` 这类后缀
直接结论是:不推荐。理由主要有三点。
1. 它和 Go 的简洁理念不一致
Go 倾向于用更少的概念表达更清楚的结构。对于一个用户领域,如果核心类型、存储逻辑和部分业务逻辑天然紧密相关,那么把它们放在同一个 user 包里,再按需要拆成少量文件,通常比机械拆成 UserService.go、UserDao.go、UserController.go 更自然。
换句话说,Go 更关注“这一组代码是不是围绕同一个领域问题”,而不是“这一段代码在传统三层架构里该挂到哪一层”。
2. Go 更强调按领域组织,而不是按技术角色组织
Java 项目里常见的思路,是先划分 Controller、Service、Dao 包,再把不同业务对象放进去。Go 的习惯恰好相反:先按功能领域划分,例如 user、order,然后在各自领域内放置相关逻辑。
这样做的好处很直接:同一领域的代码距离更近,阅读路径更短,包之间的依赖也更容易控制。读者查找用户相关逻辑时,只需要进入 user 目录,而不是在多个“技术层”目录之间来回跳转。
3. 冗余后缀会削弱命名一致性
在 Go 里,包名、目录名、文件名和类型名往往已经共同提供了足够上下文。比如一个 internal/user 目录下的 user_store.go,已经能表达“这是 user 领域里的存储相关代码”。如果再用 UserDao.go 这类命名,除了带来旧习惯的影子,并没有明显收益。
更现实的问题是,这些后缀经常会让项目逐渐走向形式上的分层整齐,却失去按业务聚合的优势。时间一长,维护者看到的是一套熟悉但笨重的结构,而不是地道的 Go 工程组织方式。
更符合 Go 习惯的项目结构是什么样
一个更常见、也更符合 Go 习惯的结构,通常会把代码按可执行入口、内部领域和可复用能力来组织:
project/
├── cmd/ // 可执行程序
│ └── myapp/
│ └── main.go
├── internal/ // 内部包,外部项目无法导入
│ ├── user/ // 按领域划分,不是按技术角色
│ │ ├── user.go // 用户领域的主要代码
│ │ ├── store.go // 用户存储相关
│ │ └── service.go // 如果有必要,可以单独文件
│ └── order/
│ ├── order.go
│ └── store.go
├── pkg/ // 可被外部项目导入的代码
│ └── util/
│ └── stringutil.go
└── go.mod
这个结构有几个要点值得注意。
按领域聚合代码
user 和 order 是领域边界,而不是技术层边界。这样组织后,一个包里可以放一组完成同一业务目标所需的代码,阅读和修改都会更顺手。
文件名保持简洁
文件名只描述当前文件的关注点,不额外背负架构标签。比如 store.go、user.go、stringutil.go 这样的名称,通常已经够用了。
减少不必要的导入和跳转
当同一领域的逻辑被放在一个包内,很多本可以在本地协作的代码,就不需要为了分层而拆成多个相互依赖的包。结果通常是导入更少,跳转更短,认知成本也更低。
代码变大时,应该怎样拆文件
不推荐 Java 风格命名,并不等于所有代码都塞进一个文件。真正合理的做法是:当某个领域的代码规模已经影响阅读和维护时,再按逻辑拆分。
文中给出的经验值是:单个文件尽量不要超过 800 行,单个函数尽量不要超过 80 行。这不是编译器强制规则,但作为工程实践上的提醒,很有参考价值。
当代码达到这个量级后,可以按职责拆分文件,但仍然应该围绕领域语义命名,而不是回到 Java 的技术后缀:
// 不推荐
user_service.go
user_controller.go
user_repository.go
// 推荐
user.go // 主要类型定义和核心方法
user_store.go // 存储相关
user_api.go // API处理相关
这里的区别很关键。user_store.go 和 user_api.go 依然是在描述“user 领域里的哪一部分内容”,而不是先假设项目必须严格映射到某种固定分层。前者更贴近 Go 的思路,也更方便后续演进。
写 Go 代码,先摆脱“上一门语言的影子”
Go 不是 Java,能写出可运行代码,不等于写出了符合 Go 习惯的代码。对于 Service、Dao、Controller 这类命名,真正的问题不是它们绝对错误,而是它们经常会把团队重新带回一套并不适合 Go 的组织方式。
更稳妥的判断标准是三点:是否足够简洁,是否按领域组织,是否符合社区约定。只要一个命名或结构同时满足这三点,它大概率就是合格的 Go 风格。
归根结底,代码首先是写给人读的,然后才是顺便交给机器执行。在 Go 里,简洁、可读性和一致性,通常比沿用 Java 时代熟悉的分层后缀更重要。









