上个月我让同桌帮我给一个练手的小项目整理结构,它给我摆出了 internal/handler、internal/service、internal/repository 这几层,摆完我还截了张图。
现在翻到那篇文章,我有点不太敢看那张图。
文章是这个月刚开头的时候在 dev.to 上刷到的,《Go Doesn't Force Clean Architecture. That's Your Job.》。我本来打算扫一眼就关——Go 我碰了两个月,属于能跑起来就高兴的阶段,谈架构实在有点越级。
结果有一句我停住了。
作者说他见过一些只用了 5 个包的项目,质量比那种目录一大片的还高,只不过它们在 GitHub 上不如后者上镜。
我当时脑子里冒出来的是「这就是在说我」。
文章开头提到的那个批评——Go 不像 Nest、不像 Django 那样告诉你东西该放哪,所以项目容易乱——这个念头我自己心里就有过。区别只是没人来问我,所以它一直没被逼着说出口。
作者的回答是:把混乱怪到语言头上,就像把烂文章怪到空白文档上。决定架构的是依赖关系,不是目录结构。
然后他给了个例子。一个项目可以把 cmd/main.go、internal/handler、internal/service、internal/repository、pkg/domain、pkg/utils 全摆得整整齐齐,然后照样烂——handler 直接调 repository、service 里 import 数据库驱动、业务逻辑里混着 HTTP 的东西、依赖成环。
目录一条没错。架构不在。
我看完就去翻了自己那个小项目。
环没找到,我那点代码量根本环不起来,说出来有点泄气。但我确实在 handler 里直接写了 SQL。当时不觉得有问题,理由是"反正就我自己用"。
现在想,"反正就我自己用"这句话,我大概用它挡过不少东西。
另一个让我卡了一会儿的是接口归谁。
我最早学到的写法是接口跟着实现走,定义在实现旁边,一个 repository 接口里 find、save、delete 摆一整套,谁用谁拿去。这篇文章说反过来了:接口是消费者定义的,service 只声明自己需要的那部分,一个只有 Find 的 UserFinder、一个只有 Save 的 UserStorage,具体那个 Postgres 实现自己回来满足它就行。你不该给自己提供的东西定义接口,你该给自己需要的东西定义接口。
这块我到现在还是夹生的。逻辑上我认,但一时想不出自己在什么场景下真的会这么写,是不是我理解反了?
而且我有个感觉——我平时写代码老想多留一点,"这个以后可能会用到",然后堆一堆暂时用不上的东西。跟目录里堆一串空文件夹,好像是同一种毛病。这个联想可能是我硬往上靠的,先记着,等哪天有实际例子再看对不对。
评论区里有两条我反复看了几遍。
有人说,文章里那句"测试还是绿的,架构已经悄悄死了"才是关键句,但靠代码评审清单来防这个,会以同样的方式死掉——清单也会被忽略。他给的办法是拿 go list -deps 写十几行 Go 测试,直接断言 internal/handler 的依赖里不出现 internal/repository,把架构规则变成构建失败,而不是一条评审意见。
然后他补了个特别具体的东西:这个测试里得加一句类似 len(deps) < 5 的保护。因为包路径写错的时候 go list 会返回一个几乎空的结果,你对着空切片循环,测试照样变绿,静默通过,跟架构腐化是同一种死法。
我当时的感觉是,原来"跑通了"和"对了"中间那条缝,哪个层面都有。
更让我意外的是作者自己的回复。他说他从没想过依赖方向可以做成机械检查,他一直是人工审查依赖注入的。
写出这篇文章的人在手工看。我第一反应是"那这方法也不彻底啊",过了几秒才反应过来我第一反应在期待什么——
我期待有个东西替我看。框架不替我看,那就找个别的来替我看。这个念头是不是有点不对劲?
再往下还有一条,讲接口开太多会让具体类型从栈跑到堆上,接着有人出来更正:把东西挪到堆上的不是接口本身,是调用点那里具体类型还看不看得见。他在 Go 1.26.2 上拿 -gcflags=-m 试,本地接口变量被调用时还能看到去虚化信息、对象不逃逸,但传给参数是接口的 noinline 函数就打印 escapes to heap。
读到"去虚化"我就跟不上了,夹生着记下。
但这两条放一起我倒是看懂一件事:评论可以改正在另一条评论。我看的教程都是一篇讲完、末尾给个结论、我抄走。这里不是。有人抛,有人补,作者自己也承认没想过。
绕回来。
我这几周其实一直在找同一样东西——某种能告诉我"你这段没写错"的东西。框架不给我,我就去问同桌。同桌很配合,它给的目录树整整齐齐,我看着心里挺舒服。
但那棵树是我要来的,不是我想出来的。依赖往哪走、接口归谁,我一格都没问过。
写得有点丧。不过这么想也踏实一点:目录是抄的,依赖是自己走的。抄来的那部分骗不了编译器,也骗不了三个月后回来改代码的我。
明天想干的事很小:先不搬目录,把我现在每个文件 import 了谁一个个列出来,列在纸上。列到一半列不下去,那就说明我根本没读懂自己写的是什么。
还没开始列。先这样。
