Go 单体服务怎么分层,一个人维护够用

当一个人的服务从几个接口长到几十个接口时,最先失控的不是性能,是「这个逻辑该写在哪」。Go 单体服务怎么分层比较合理,按我的经验四层就够了——目标不是架构漂亮,是让你三个月后还能一眼找到代码该放哪。

一、结论:Go 单体服务怎么分层比较合理,四层各管一段

这四层是:入口层、业务层、数据层、以及一个放通用能力的地方。

入口层只做请求解析和响应组装,业务层放规则判断和流程编排。数据层只管读写存储,通用能力放日志、配置、错误定义这类被所有层用到的东西。

关键在于依赖只往下走:入口依赖业务,业务依赖数据。反过来就乱了。

二、前提:什么情况下适用

这套分法适合接口数量在几十个量级、由一到两个人维护的服务。

如果接口只有个位数,硬分四层反而是负担——每个接口要穿过四个文件才能看完,读起来更累。

如果是多人协作、模块边界已经很清晰,那可能需要按领域切得更细。但别在项目早期就按那个规模设计。

三、分步走

第一步,先定目录结构,而不是先写代码。

按层建目录,每个目录里放这一层的东西。这一步的目的是让「新代码放哪」有一个默认答案,不用每次讨论。

第二步,把边界定在数据的形状上。

入口层收到的请求结构体和数据层读出来的记录,不要是同一个类型。中间隔一层,各自变化互不影响。

这一步很多人嫌麻烦跳过。跳过的后果是:数据库加一个字段,接口的返回就跟着变了。这类耦合一次埋下去,后面每次改都要连着改两处。

第三步,业务层只依赖数据层的接口,不依赖具体实现。

业务层调数据层时面向一个抽象,而不是具体的存储类型。这样以后换存储、或者在测试里塞一个假的实现,都不用改业务代码。

第四步,通用能力单独一层,但别什么都往里塞。

这一层最容易变成垃圾场。判断标准是:被两个以上层用到的东西才进来。

四、容易卡住的三个点

第一个是不知道该放哪。判断口诀:这段代码是在处理外部世界的形状(入口)、在做规则判断(业务)、还是在跟存储打交道(数据)。三个都不是,那多半是通用能力。

第二个是业务层越来越胖。一个业务函数几百行,什么都在里面。按流程拆成几个小函数,但别跨层拆。

第三个是循环依赖。A 层调 B 层,B 层又回头调 A 层,出现了说明边界画错了。Go 里循环依赖会直接编译不过,这算它帮忙挡住了一部分问题。

五、怎么验证分对了

一个简单的验证:随便挑一个接口,看你能不能只靠文件名就说出它的请求从哪进、规则写在哪、数据从哪来。说不出来,说明分层还没落地,只是建了几个目录。

另一个验证是改动的扩散范围:加一个字段、改一条规则,理想情况下只需要动一到两个文件。如果一个字段的改动要动五个文件,那就是层与层之间粘得太紧。

六、我现在的取舍

我自己的后台服务是四层,但通用能力那一层我刻意保持很薄。薄的好处是打开项目时不会被一堆工具类淹没,能直接看到业务在哪里。

另外,我没有引入任何依赖注入框架。手动在启动的地方把各层拼起来,几十行代码,比框架好懂得多。一个人维护的项目,可读性比少写几行更重要,这也是 Go 单体服务怎么分层比较合理这件事上我最看重的一条。

常见问题

Q:Go 单体服务怎么分层比较合理?

A:入口层、业务层、数据层、通用能力四层,依赖只允许往下走。接口量在几十个、一到两人维护的规模,这套够用。

Q:小项目也要分层吗?

A:接口只有个位数时不必。硬分会让你看一个接口要翻四个文件,反而更难读。等接口多起来、或者出现重复逻辑时再分。

Q:分层之后代码量会暴涨吗?

A:会多出一些类型定义和转换代码。收益是改一处只动一处,边界清楚。这个交换在项目活过三个月之后是划算的。

相关阅读


本文作者西米,37 岁程序员,一个人加五个 AI 员工在跑一家公司。踩过的坑和真实账本都写在公众号「西米分享」。

完整代码、踩坑清单与最新进展,都在公众号。

📱 关注公众号「西米分享」