Go语言有哪些“劣势”_Go语言

本文源于笔者对知乎上的一个问题“Go 有哪些劣势?”[1]的一次回答[2]。当时随手花几分钟在手机上写了一些点。但事后我觉得应该再做一些系统地思考。在这里我就将更系统地思考后的答案整理并分享给大家。

关于 Go 语言,我是喜欢的,甚至可以算作“鼓吹者”阵营的一份子。但我一贯秉承“Go 并非完美语言”这个观点来尽可能客观地看待 Go。每种编程语言都有自己的劣势,Go 也不例外,下面我们就来列举一下 Go 的那些“劣势”:

1. 技术路线选择导致的“性能劣势”

众所周知,Go 是带垃圾回收的编程语言,因此不管 Go 的 STW(Stop The World)的时间有多么短,GC 的延迟有多么的小,它依然属于 GC 类编程语言,和 Java、C#属于一个阵营,同时天然与 C、C++、Rust 这样的手动管理内存、没有运行时 GC 负担的编程语言之间划清了界线。虽然Go 语言的初衷是成为系统级编程语言[3](关于 Go 语言的诞生语言演化历史,可以参考我的技术专栏文章“Go 语言的前生今世”,虽然 Go 的运行时性能可以满足 99.99%的场合的需要,虽然百度的万亿流量转发引擎 BFE[4]、时序数据库influxdb[5]、分布式关系数据库TiDB[6]等性能敏感的项目都选择了用 Go 实现,但不能否认的是在一些性能超级敏感的场合,选择 Go 依然要慎重。

2 坚持自己的设计哲学所带来的“表达劣势”

1) “单一”的表达方法

很多从其他语言转到 Go 阵营的开发人员抱怨Go 能玩的花样太少,套路不多,Go 之所以表现出“表达劣势”,源于其设计哲学中的一个原则:“崇尚一个事情只有一个或少数几种写法”。这个原则不符合某些开发人员炫技的心理需求,于是 Go 就被诟病为是资质平平的程序员才会去用的语言。

Go 1.18 将加入泛型(类型参数),这算是对此类“劣势”的一个“弥补”。不过对于我们这些对 Go 价值观和设计哲学认同已久的 Gopher 而言,我们十分担心大幅提高 Go 表达能力的泛型将成为奇技淫巧的“滋生地”。

2) “过时”的显式的错误处理

Go 语言从诞生那天起就没有像 C++、Java、Python 等主流编程语言那样提供基于异常(exception)的结构化 try-catch-finally 错误处理机制,Go 的设计者们认为将异常耦合到程序控制结构中会导致代码混乱[7]。Go 提供了一种简单的基于错误值比较的错误处理机制,这“强迫”每个 Go 开发人员都必须显式地去关注和处理每个错误,经过显式错误处理的代码会更为健壮,也会让 Go 开发人员对这些代码更有信心。但这一设计哲学的坚持却被很多来自其他语言的开发者嘲笑为“过时”,被称为“半个世纪前的古老机制”。(笔者注:十九世纪 70 年代 C 语言诞生时采用的错误处理机制)

Go 开发团队也曾“动摇过”,Go 开发团队在发布 Go2 计划后曾发布过多版Go 错误处理的新机制草案[8]。Go 社区也针对此问题做过长时间的讨论甚至是“争吵”,知名 Gopher Dave Cheney 发声、Rob Pike 发声,著名 Go 培训师、《Go 语言实战》联合作者之一的威廉·肯尼迪(William Kennedy)更是在 Go 团队 try 提案公示之后,发表了对 Go 社区的公开信反对 try 方案(更多内容可参考笔者的专栏文章“if err != nil 重复太多可以这么办”[9],最终坚持 Go 设计哲学的一派占据了上风,try 提案被否决,没有加入到Go 1.13 版本中!

3. 背离主流的“小众劣势”

Go 早期设计的包依赖管理机制的确存在不小的“瑕疵”,这源于 Google 内部大单一代码仓库与基于主干的开发模型的影响。走出 Google 的 Go 语言听到了不同方面的声音,Go 包管理机制长期无法满足社区的需求。于是先后出现了vendor 机制[10]、dep[11]等对包依赖管理的改进尝试。

2018 年初,正当广大 gopher 们认为 dep 将“顺理成章”地升级为 go 官方工具链的一部分的时候,Go 核心团队的技术负责人,也是 Go 核心团队早期成员之一的 Russ Cox 在个人博客上连续发表了七篇文章[12],系统阐述了 Go 团队解决“包依赖管理” 的技术方案: vgo[13],即 go module 的前身。

vgo 的主要思路包括:语义导入版本 (Semantic Import Versioning)、 最小版本选择 (Minimal Version Selection) ,这些都与当前主流编程语言的包依赖管理的规则相悖,尤其是最小版本选择(MVS)[14],算是另辟蹊径,背离主流!(更多关于 go module 最佳实践的内容可以参考我的专栏文章“与时俱进!使用 module 管理依赖包”[15])。

4. Go 核心团队的“民主集中制”导致的“社区劣势”

和 Rust 团队广泛采纳社区建议“猛加语言特性”不同,Go 像是另外一个极端:Go 核心团队对语言演化的把控力十足,不是社区多数人赞同的就一定会被采纳而加入 Go 语言,我这里将其戏称为“民主集中制”吧,即真正的投票权其实在 Go 核心团队的代表社区的少数人手中。

2018 年初的 dep 与 vgo 之争就是这一“劣势”的典型表现。社区费劲一年多努力精心打造的 dep 项目被 Russ Cox 等少数人集中花掉一些时间设计出的 vgo 给“挤出”了 Go 包依赖管理工具标准的位置,成为了 Go module 成功的“垫脚石”。即便最终证明 Go 团队使用 go module 的决策的结果是正确的,但 这导致的 Go 社区与 Go 核心团队的“裂痕”是确确实实存在的,以致于这两年 Go 核心团队极力改善与 Go 社区的关系,规范化和透明化 Go proposal 的提出、review 和接纳流程。

5. 全面出击失败后,期望的落空导致的“功能孱弱劣势”

Go 1.5 发布之后,由于实现了自举和 GC 延迟的大幅下降,Go 受关注程度逐渐升高,直至 2017 年初第二次拿到 TIOBE 年度最佳编程语言,让 Go 语言有些“膨胀”,甚至狂热的 Go 鼓吹者曾一度希望 Go 一统江湖:不仅牢牢把持住自己的云原生市场,占领 Java 的企业级市场,还要在终端(android. ios)、前端(js)上击败现有对手。

有人可能觉得我的上述说法可笑,但这些说法并非空穴来风。Go 语言在终端、前端方面还真的曾经发过力,了解 Go 历史的都知道,Go 团队曾经有全职开发人员参与gomobile 项目[16](http://golang.org/x/mobile),该项目旨在构建在 Android 和 iOS 上的 Go 技术栈,实现用 Go 语言编写终端应用的目的。

在前端方面,gopherjs 项目[17](https://github.com/gopherjs/gopherjs)可以将 go 代码编译为 js 代码并运行于各大浏览器中。后来 gopherjs 的作者又帮助 go 项目原生支持 webassembly,支持将 go 编译为 webassembly 运行在浏览器中。

但上面的尝试最终没能“得偿如愿”,现状是在终端、前端应用领域,使用 Go 编码的人少之又少。于是 Go 又逐渐冷静下来,回到自己擅长的主力战场,回归到了企业级应用、基础设施、中间件、微服务、命令行应用等领域,并且在这些领域取得了越来越多开发者的青睐。

但曾经的全面出击失败给很多开发者留下了“Go 功能孱弱”的口实,甚至有人说亲爹 Google也没能让亲兄弟 Android 给 Go 走个后门。

小结

记得有人问过 Go 核心开发团队这样一个问题:未来 Go 语言演化之路上最困难的事情是什么?Go 团队的回答是:使 Go 语言一直保持简单。

在本文列出的几点“劣势”中,除了第一点的性能劣势和最后两点有待商榷外,其他几点对于不爱 Go 的开发人员来说,这些的确都是“劣势”。但对于真正认同 Go 价值观和设计哲学的开发者而言,这些难道不正是 Go 语言的“优势”吗!