Golang是否还要维持自己的哲学?


Golang是一门非常opinionated的语言。

它不喜欢”魔法”。很多事情都要求开发者手动写清楚:错误处理要显式处理,依赖关系最好摆在代码里,抽象也尽量不要藏得太深。

这里说的”魔法”,不是说它真的神秘,而是指那些从代码表面看不太出来具体运作原理,只告诉你”它能做什么”的功能。比如框架约定、注解、隐式依赖注入,或者大量隐藏在运行时里的行为。

这种哲学在早期的Go里体现得非常明显。比如很长一段时间里,Go并不支持泛型。

我记得1.6左右的时候,我们需要写一个自己的语言。用yacc解析之后,还要处理各种token和语法节点。因为缺少泛型和更强的抽象能力,很多地方都得靠type assertion去猜具体类型,再一层层处理错误。

那真的是非常痛苦的开发体验。

Go的克制,确实带来了好处

但我并不是在说这种practice不好。实际上,它带来了很大的好处:不管你是新手还是老手,你写出来的代码通常都能让别人读懂。

没有过度的抽象,也不用为了看一个implementation在好几个interface之间来回跳。

如果换成C++或者Java,老手和新手写出来的代码往往是天壤之别。读懂老手的代码,本身就有门槛。Go的哲学降低了这个门槛,项目更容易让新人进来维护。

只要愿意花时间,代码大概率是能看明白的。

这种practice在小规模,甚至中等规模的项目里都是优势。在大量中间件开发场景,或者高并发的queue、调度器之类的场景里,它也非常好用。

也正因为如此,很多公司和团队在2018年前后转向了Golang。

但是,当公司的业务代码遇到Golang时,它有自己的好处,也暴露出了自己的瓶颈。

先说好处。

和过去的优势一样,代码容易写,也容易读。公司实际上不需要专门招会Golang的人,只需要招有经验的后端程序员,训练三个月基本就可以放心让他们写业务代码。

这不仅让项目好维护,对于Golang生态也是一大利好。和同样容易写的Python相比,Golang在类型系统、gofmt、依赖管理和交叉编译上都有明显优势。

另一个好处是Go的并发模型。

业务代码里经常会遇到需要并发的地方。过去用Java或者C++写这类并发业务逻辑,通常都比较麻烦。Go语言的goroutine,加上errgroupsync.WaitGroup这类工具,极大降低了心智负担,让concurrency不再那么复杂。

但业务代码里的Go,也会变得很重

接下来就要说坏处了。

首先,冗长的代码会极大降低信息密度。

当你需要快速扫过一些Golang代码时,经常会觉得屏幕不够用。大脑会自动屏蔽大量错误处理相关的逻辑,它们就像一块块空白,占据了代码很大的篇幅。

读这样的代码有时像是在读流水账,里面有大量必要但低信息量的内容。

我这里并不是在说Golang的错误处理不好。实际上,我觉得手动处理错误是非常好的事情。但客观来说,Go代码的冗余有相当一部分都是由显式错误处理造成的。

其次,是Golang的”简单”哲学,或者说”克制”哲学。

就拿泛型举例。在1.18之前的Go里,你确实可以用大量类型区分、重复方法,或者代码生成来达成类似效果。但这样势必会让代码的可维护性下降。

想象一台机器,上面有100个按钮。通过这些按钮的on/off组合,你可以达成不同效果。

很多语言提供的语法糖,就像是允许你把常见组合封装成模板,让使用者不必每次都记住所有按钮怎么按。而Golang的态度更接近于:

不,我们希望你完全了解自己在做什么。

于是大家只能手动操作这些按钮。

诚然,前者可能会带来很多问题。错误的抽象会让使用者误解抽象背后的事物,也可能让调用方丧失深度定制的能力。

但是,完全暴露所有抽象内部的东西,也不一定是好选择。

人的脑子是有限的,维护代码的人也会变。完全依靠使用者自己的判断,一样会造成很多不必要的事故。

这不是一个纯粹的语言问题

说得难听一点,Golang的设计者不想承担过多”工具”的责任。

完全暴露内部细节,有时像是一种甩手掌柜式的设计:选择都给你了,你自己负责。

然而,不是所有程序员都是高技能程序员,也不是所有人都能对语言和业务逻辑保持足够深刻的理解。最后,这些代码就会变成不断重复、冗长的代码。即使团队里有经验丰富的成员,也很难一直为其他人兜底。

我在说的不是一个纯粹的PL问题,不是一个科学问题,而是一个工程问题,也是一个管理问题。

没有魔法固然有好处。我自己也非常讨厌魔法,因为学习魔法的过程往往很畸形:你只能通过不断实验来猜测内部到底怎么运行。

可是,魔法的好处也同样明显。

它能让业务程序员把注意力从纯粹的代码语法中抽离出来,更多关注业务流程和逻辑本身。这对于开发中间件可能没那么重要,但对于开发电商、支付、CRM这类业务系统非常重要。

为了让junior member能在团队中交付价值,我们需要让他们少花一点时间纠结手动依赖注入,少花一点时间重复写for loop里的样板代码,而是更多关注数据如何流动、业务规则如何表达。

Go站在十字路口

所以,Golang这门语言确实走到了一个十字路口。

它需要决定自己想成为什么样的语言。

如果想占领Java、C#的位置,那它势必需要接受更多抽象,甚至接受一部分”魔法”。

如果想继续保持极致简单,那它可能需要更轻量的脚本式使用方式,让小工具、小任务不用总是进入完整工程模式。

如果想变得更加安全,那么Rust可能是一个值得学习的方向。

而现在的Golang站在这个十字路口,手里握着自己的哲学,却还在原地踌躇。