Go 语言十年而立,Go2 蓄势待发
TODO: golang scheduler 相关的内容
Go 语言十年而立,Go2 蓄势待发
在 21 世纪的第一个十年,计算机在中国大陆才逐渐开始普及,高校的计算机相关专业也逐渐变得热门。当时学校主要以 C/C++和 Java 语言学习为主,而这些语言大多是上个世纪 90 年代或更早诞生的,因此这些计算机领域的理论知识或编程语言仿佛是上帝创世纪时的产物,作为计算机相关专业的学生只能仰望这些成果。
Go 语言诞生在 21 世纪新一波工业编程语言即将爆发的时期。在 2010 年前后诞生了编译型语言 Rust、Kotlin 和 Swift 语言,前端诞生了 Dart、TypeScript 等工业型语言,最新出现的 V 语言更甚至尝试站在 Go 和 Rust 语言肩膀之上创新。而这些变化都发生在我们身边,让中国的计算机爱好者在学习的过程中见证历史的发展,甚至有机会参与其中。
2019 年是 CSDN 的二十周年,也是 Go 语言面世十周年。感谢 CSDN 平台提供的机会,让笔者可以跟大家分享十年来中国 Go 语言社区的一些故事。
Go 语言诞生
Go 语言最初由 Google 公司的 Robert Griesemer、Ken Thompson 和 Rob Pike 三位大牛于 2007 年开始设计发明的。**其设计最初的洪荒之力来自于对超级复杂的 C++11 特性的吹捧报告的鄙视,最终目标是设计网络和多核时代的 C 语言。**到 2008 年中期,语言的大部分特性设计已经完成,并开始着手实现编译器和运行,大约在这一年 Russ Cox 作为主力开发者加入。到了 2009 年,Go 语言已经逐步趋于稳定。同年 9 月,Go 语言正式发布并开源了代码。
以上是《Go 语言高级编程》一书中第一章第一节的内容。Go 语言刚刚开源的时候,大家对它的编译速度印象异常深刻:秒级编译完成,几乎像脚本一样可以马上编译并执行。同时 Go 语言的隐式接口让一个编译型语言有了鸭子类型的能力,笔者也第一次认识到原来 C++的虚表 vtab 也可以动态生成!至于大家最愿意讨论的并非特性,其实并不是 Go 语言新发明的基石,早在上个世纪的八九十年代就有诸多语言开始陆续尝试将 CSP 理论引入编程语言(Rob Pike 是其中坚定的实践者)。只不过早期的 CSP 实践的语言没有进入主流开发领域,导致大家对这种并发模式比较陌生。
除了语言特性的创新之外,Go 语言还自带了一套编译和构建工具,同时小巧的标准库携带了完备的 Web 编程基础构建,我们可以用 Go 语言轻松编写一个支持高并发访问的 Web 服务。
作为互联网时代的 C 语言,Go 语言终于强势进入主流的编程领域。
Go 语言十年奋进
Go 从 2007 年开始设计,在 2009 年正式对外公布,至今刚好十年。十年来 Go 语言以稳定著称,Go1.0 的代码在 2019 年依然可以不用修改直接被编译运行。但是在保持语言稳定的同时,Go 语言也在逐步夯实基础,十年来一直向着完美的极限逼近。让我们看看这十年来 Go 语言有哪些变化。
界面变化
首先是看看界面的变化。第一次是在 2009 刚开源的时候,这时候可以说是 Go 语言的上古时代。Go 语言的主页如下:
那个年代的 Gopher 们,使用的是 hg 工具下载代码(而不是 Git),Go 代码是在 Google Code 托管(而不是 GitHub)。随着代码的发展,hg 已经慢慢淡出 Gopher 视野,Google Code 网站也早已经关闭,而 Go1 之前的上古时代的 Go 老代码已经开始慢慢腐化了。
首页中心是 Go 语言最开始的口号:Go 语言是富有表现力的、并发的编程语言,并且是简洁的。同时给了一个“Hello, 世界”的例子(注意,这里的“世界”是日文)。
然后右上角是初学者的乐园:首先是安装环境,然后可能是早期的三日教程,第三个是标准库的使用。右上角的图片是 Russ Cox 的一个视频,在 Youtube 应该还能找到。
左上角是 Go 实战的那个经典文档。此外 FAQ、语言规范、内存模型是非常重要的核心温度。左下角还有 cmd 等文档链接,子页面的内容应该没有什么变化。
然后在 2012 年准备发布第一个正式版本 Go1,在 Go1 之前语言、标准库和 godoc 都进行了大量的改进。Go1 风格的页面效果如下:
新页面刚出来的时候有眼睛一亮的感觉,这个是目前存在时间最长久的页面布局。但是不仅仅是笔者我,甚至 Go 语言官方也慢慢对中国页面有点审美疲劳了。因此,从 2018 年开始 Go 语言开始新的 Logo 和网站的重新设计工作。
下面的是 Go 语言新的 Logo:
2019 年是对 Go 语言发展极其重要的一年,今年 8 月将发布 Go1.13,而这个版本将正式重启 Go 语言语法的进化,向着 Go2 前进。而新的网站已经在 Go1.13 正式发布之前的 7 月份就已经上线:
头部的按钮风格的菜单变成了平铺的风格,显得更加高大上。同时页面的颜色做了调整,保持和新 Logo 颜色一致。页面的布局也做了调整,将下载左右两列做了调换。同时地鼠的脑袋歪到一边,估计是颈椎病复发了。
总的来说,Go 语言官网主页经历了 Go1 前、Go1(1.0 ~ 1.10)、Go1 后(或者叫 Go2 前)三个阶段,分别对应 3 种风格的页面。新的布局或许会成为下个十年 Go2 的主力页面。
语法变化
Go 语言虽然从 2009 年诞生,但是到了 2012 年才发布第一个正式的版本 Go1。其实在 Go1 诞生之前 Go 语言就已经足够稳定了,国内的七牛云从 Go1 之前就开始大力转向 Go 语言开发,是国内第一家广泛采用 Go 语言开发的互联网公司。Go1 的目标是梳理语法和标准库阴暗的角落,为后续的 10 年打下坚实的基础。
从目前的结果看,Go1 无疑是取得了极大的成果,Go1 时代的代码依然可以不用修改就可以用最新的 Go 语言工具编译构建(不包含 CGO 或汇编语言部分,因为这些外延的工具并不在 Go1 的承诺范围)。但是 Go1 之后依然有一些语法的更新,在 Go1.10 前的 Go1 时代语法和标准库部分的重大变化主要有三个:
第一个重大的语法变化是在 2012 年发布的 Go1.2 中,给切片语法增加了容量的控制,这样可以避免不同的切片不小心越界访问有着相同底层数组的其它切片的内存。
**第二个重大的变化是 2016 年发布的 Go1.7 标准库引入了 context 包。**context 包是 Go 语言官方对 Go 进行并发编程的实践成果,用来简化对于处理单个请求的多个 Goroutine 之间与请求域的数据、超时和退出等操作。context 包推出后就被社区快速吸收使用,例如 gRPC 以及很多 Web 框架都通过 context 来控制 Goroutine 的生命周期。
**第三个重大的语法变化是 2017 年发布的 Go1.9 ,引入了类型别名的特性:****type T1 = T2。**其中类型别名 T1 是通过=符号从 T2 定义,这里的 T1 和 T2 是完全相同的类型。之所以引入类型别名,很大的原因是为了解决 Go1.7 将 context 扩展库移动到标准库带来的问题。因为标准库和扩展库中分别定义了 context.Context 类型,而不同包中的类型是不相容的。而 gRPC 等很多开源的库使用的是最开始以来的扩展库中的 context.Context 类型,结果导致其无法和 Go1.7 标准库中的 context.Context 类型兼容。这个问题最终通过类型别名解决了:扩展库中的 context.Context 类型是标准库中 context.Context 的别名类型,从而实现了和标准库的兼容。
此外还有一些语法细节的变化,比如 Go1.4 对 for 循环语法进行了增强、Go1.8 放开对有着相同内存布局的结构体强制转型限制。读者可以根据自己新需要查看相关发布日志的文档说明。
运行时的变化
运行时部分最大的变化是动态栈部分。在 Go1.2 之前 Go 语言采用分段栈的方式实现栈的动态伸缩。但是分段式动态栈有个性能问题,因为栈内存不连续会导致 CPU 缓存命中率下降,从而导致热点的函数调用性能受到影响。因此从 Go1.3 开始该有连续式的动态栈。连续式的动态栈虽然部分缓解了 CPU 缓存命中率问题(依然存在栈的切换问题,这可能导致 CPU 缓存失效),但同时也带来了更大的实现问题:栈上变量的地址可能会随着栈的移动而发生变化。这直接带来了 CGO 编程中,Go 语言内存对象无法直接传递给 C 语言空间使用,因此后来 Go 语言官方针对 CGO 问题制定了复杂的内存使用规范。
总体来说,动态栈如何实现是一个如何取舍的问题,因为没有银弹、鱼和熊掌不可兼得,目前的选择是第一保证纯 Go 程序的性能。
GC 性能改进
Go 语言是一个带自动垃圾回收的语言(Garbage Collection ),简称 GC(注意这是大写的 GC,小写的 gc 表示 Go 语言的编译器)。从 Go 语言诞生开始,GC 的回收性能就是大家关注的热点话题。
Go 语言之所以能够支持 GC 特性,是因为 Go 语言中每个变量都有完备的元信息,通过这些元信息可以很容易跟踪全部指针的声明周期。在 Go1.4 之前,GC 采用的是 STW 停止世界的方式回收内存,停顿的时间经常是几秒甚至达到几十秒。因此早期社区有很多如何规避或降低 GC 操作的技巧文章。
第一次 GC 性能变革发生在 Go1.5 时期,这个时候 Go 语言的运行时和工具链已经全部从 C 语言改用 Go 语言实现,为 GC 代码的重构和优化提供了便利。Go1.5 首次改用并行和增量的方式回收内存,这将 GC 挺短时间缩短到几百毫秒。下图是官网“Go GC: Latency Problem Solved”一文给出的数据:
Go1.5 并发和增量的改进效果明显,但是最重要的是为未来的改进奠定了基础。在 Go1.5 之后的 Go1.6 版本中 GC 性能终于开始得到了彻底的提升:从 Go1.6.0 停顿时间降低到几十毫秒,到 Go1.6.3 降低到了十毫秒以内。而 Go1.6 取得的成果在 Go1.8 的官方日志得到证实:Go 语言的 GC 通常低于 100 毫秒,甚至低于 10 毫秒!
当然,Go 的 GC 优化的脚步不会停止,但是想再现 Go1.5 和 Go1.6 时那种激动人心的成果估计比较难了。在 Go1.8 之后的几个版本中,官方的发布日志已经很少再出现量化的 GC 性能提升数据了。
Go 语言自举历程
据说 Go 语言刚开始实现时是基于汤普森的 C 语言编译改造而成,并且最开始输出的是 C 语言代码(还没有对外公开之前)。在开源之后到 Go1.4 之前,Go 语言的编译器和运行时都是采用 C 语言实现的。以至于早期可以用 C 语言实现一个 Go 语言函数!因为强烈依赖 C 语言工具链,因此 Go1.4 之前 Go 语言是完全不能自举的。
从 Go1.4 开始,Go 语言的运行时采用 Go 语言实现。具体实施的方式是 Go 团队的 rsc 首先实现了一个简化的 C 代码到 Go 代码的转换工具,这个工具主要用于将之前 C 语言实现的 Go 语言运行时转换为 Go 语言代码。因为是自动转换的代码,因此可以得到比较可靠的 Go 代码。运行时转换为 Go 语言实现之后,带来的第一个好处就是 GC 可以精确知道每个内存指针的状态(因为 Go 语言的变量有详细的类型信息),这也为 Go1.5 重写 GC 提供了运行时基础。
然后到了 Go1.5,将编译器也转为 Go 语言实现。但是转换到代码性能有一定的下降。很多程序的编译时间甚至缓慢到几十秒,这个时期网上出现了很多吐槽 Go1.5 编译速度慢的问题。Go1.5 采用 Go 语言编写编译器的同时,对工具链和目标代码都做了大量的重构工作。从 Go1.5 之后,交叉编译变得异常简单,只要 GOOS=linux GOARCH=amd64 go build 命令就可以从任何 Go 语言环境生成 Linux/amd64 的目标代码。
Go 语言从 Go1.4 到 Go1.5,经历了两个版本的演化终于实现了自举的支持。当然自举也会带来一个哲学问题:Go 语言的编译器是否有后门?如果有后门的编译器编译出来的 Go 程序是否有后门?有后门的编译器编译出来的 Go 编译器程序是否有后门?
失败的尝试
Go 语言发展过程中也并不全是成功的案例,同时也存在一些失败的尝试。失败乃成功之母,这些尝试虽然最终失败了,但是在尝试的过程之中积累的经验为新的方向提供了前进的动力。
**因为 Go 语言的常量只支持数值和字符串等少数几个类型,早期的社区中一直呼吁为切片增加只读类型。**为此 rsc 在开发分支首先试验性地实现了该特性,但是在之后的实践过程中又发现了和 Go 编程特性冲突的诸多问题,以至于在短暂的尝试之后就放弃了只读切片的特性。当然,初始化之后不能修改的变量特性依然是大家期望的一个特性(类似其它语言的 final 特性),希望在未来的 Go2 中能有一定的改善。
**另一个尝试是早期基于 vendor 的版本管理。**在 Go1.5 中首次引入 vendor 和 internal 特性,vendor 用于打包外部第三方包,internal 用户保护内部的包。后来 vendor 被开源社区的各种版本管理工具所滥用,导致 Go 语言代码经常会出现一些不可构建的诡异问题。滥用 vendor 导致了 vendor 嵌套的问题,这和 nodejs 社区中 node_modules 目录嵌套的问题类似。嵌套的 vendor 中最终会出现同一个包的不同版本,这根最后的稻草终于彻底击溃了 vendor 机制,以至于 Go 语言官方团队从头开发了模块特性来彻底解决版本管理的问题。等到 Go1.13 模块化特性转正之中,GOPATH 和 vendor 等机制将被彻底淘汰。
Go 语言作为一个开源项目,所有导入的包必须有源代码。一些号称是商业用户,呼吁 Go 语言支持二进制包的导入,这样可以最大限度地保护商业代码。为了响应社区的需求,Go1.7 增加了导入二进制包的功能。但是比较戏剧化的是,Go 语言支持二进制包导入之后并没有多少人在使用,甚至当初呼吁二进制包的人也没有使用(所以说很多社区的声音未必能够反映真实的需求)。为了一个没有人使用的二进制包特性,需要 Go 语言团队投入相当的人力进行维护代码。为了减少这种不需要的特性,Go1.13 将彻底关闭二进制包的特性,从新轻装上阵解决真实的需求。当然,Go 语言也已经支持了生成静态库、共享库和插件的特性,也可以通过这些机制来保护代码。
失败的尝试可能还有一些,比如最近 Go 语言之父之一 Robert Griesemer 提交的通过 try 内置函数来简化错误处理就被否决了。失败的尝试是一个好的现象,它表示 Go 语言依然在一些新兴领域的尝试——Go 语言依然处于活跃期。
Go2 的发展方向
Go 语言原本就是短小精悍的语言,经过多年的发展 Go1 已经逼近稳定的极限。查看官网的 Talk 页面的报告数量可以发现,2015 年之前是各种报告的巅峰,2016 到 2017 年分享数量已经开始急剧下降,2018 年至今已经没有新的报告被收录,这是因为该讲的 Go1 语言特性早就被讲过多次了。对于第一波 Go 语言爱好者来说也是如此,Go 语言已经没有什么新的特性可以挖掘和学习了,或者说它已经不够酷了。我们想 Go 语言官方团队也是这样的感觉,因此从 2018 年开始首先开始解决模块化的问题,然后开始正式讨论 Go2 的新特性,并且从 Go1.13 重新启动语言的进化。
模块化和构建管理有关系。在 Go 语言刚刚诞生之初,其实是通过一个 Makefile 目标进行构建。然后官方提供了 go build 命令构建,实现了零配置文件构建,极大地简化了构建的流程。再后来出现了 go get 命令,支持从互联网上自动下载 hg 或 git 仓库的代码进行构建,并同时引入 GOPATH 环境变量来防止非标准库的代码。此后,第一波的版本管理工具也开始出现,通过动态调整 GOPATH 实现导入特定版本的代码。随后各种开源模仿、克隆的版本管理工具如雨后春笋般冒出来,基本都是模仿 godeps 的设计思路,基于 GOPATH 和后来的 vendor 来管理依赖包的版本,这也最终导致了 vendor 被过度滥用(前文已经讲过 vendor 滥用带来的问题)。最终在 2018 年,由 rsc 亲自操刀从头发明了基于最小化版本依赖算法的版本管理特性。模块化特性从 Go1.11 开始引入,将在 Go1.13 版本正式转正,以后 GOAPATH 将彻底退出历史舞台。
因为 rsc 的工作直接宣判了开源社区的各种版本管理工具的死亡,这也导致了 Go 语言官方团队和开源社区的诸多冲突和矛盾。在此需要补充说明下,Go 语言的开发并不完全是开源陌生,Go 语言的开源仅仅限于 Issue 的提交或 BUG 的修改,真正的语言设计始终走的是教堂元老会的模式。笔者以为这是最好的开源方式,很多开源社区的例子也说明了需要独裁者的角色,而元老会正是这种角色。
在 Go1.13 中,除了模块化特性转正之外,还有诸多语法的改进:比如十六进制的浮点数、大的数字可以通过下划线进行分隔、二进制和八进制的面值常量等。但是 Go1.13 还有一个重大的改进发生在 errors 标准库中。errors 库增加了 Is/As/Unwrap 三个函数,这将用于支持错误的再次包装和识别处理,是为了 Go2 中新的错误处理改进提前做准备。后续改进方向就是错误处理的控制流,之前已经出现用 try/check 关键字和 try 内置函数改进错误处理流程的提案,目前还没有确定采用什么方案。
**Go2 最期待的特性是泛型。**从开始 Go 语言官方明显抵制泛型,到 2018 年开始公开讨论泛型,让泛型的爱好者看到了希望。很多人包括早期的 Go 官方都会说用接口模拟泛型,这其实只是一个借口。泛型最大的问题不在于性能,而是只有泛型才能够为泛型容器或算法提供一个类型安全的接口。比如一个 Add(a, b T) T 泛型函数是无法通过接口来实现对返回值类型的检查的。如果 Go 语言支持了泛型,再结合 Go 语言汇编语言支持的 AVX512 指令,可以期待 Go 语言将在 CPU 运算密集型领域占有一席之地,甚至以后会出现纯 Go 语言的机器学习算法库的实现。
**最后一个值得关注的是 Go 语言对 WebAssembly 平台的支持。**根据 Ending 定律:一切可编译为 WebAssembly 的,终将会被编译为 WebAssembly。2018 年,Fabrice Bellard 大神基于 WebAssembly 技术,将 Windows 2000 操作系统搬到了浏览器环境运行。2019 年出现了 WebAssembly System Interface 技术,这很可能是一个更轻量化的 Docker 替代技术。而 Go 语言也出现了一个变异版本 TinyGo,目标就是为了更好地在 WebAssembly 或其它单片机等受限环境运行 Go 程序。
Go 语言在中国
回想 Go 语言刚面世时的第一个例子,是打印"Hello, 世界"。只可惜这里的“世界”并不是中文的“Hello, 世界”,而是日文的“Hello, 世界”。而日文还是基于中文汉字改造而来,这是整个中文世界的悲哀!
比较庆幸的是中国程序员比较给力,目前中国不仅仅是世界上 Go 语言关注度最高的国家,也是贡献排名第二的国家。根据谷歌趋势的数据,Go 语言在中国的关注度占全球的 90%以上:
不仅仅是 Go 语言用户,中国的 Gopher 对 Go 语言的贡献也稳居美国之后。其中韦京光早在 2010 年就深度参与 Go 语言开发,将 Go 语言移植到 Windows 系统并实现了 CGO 支持。之后来自中国的 Minux 实现了 iOS 等诸多平台的移植,并已经正式加入 Go 语言开发团队。而目前 Go 语言中国贡献者排名第一的是来自天津的史斌(benshi001),他的很多工作集中在编译的优化方面,在全球 Go 语言贡献者排名第 39 位。
最早 Go 语言中文爱好者都是通过谷歌讨论组 golang-china 讨论,目前该讨论组还陆续会有新的文章发布。然后到了 2012 年前后,因为诸多因素国内的讨论开始集中到 QQ 群中(笔者在 2010 年建立了国内第一个 Go 语言 QQ 讨论群)。再往后就是微信各种论坛遍地开花了。十年来,Go 语言中文社区也一直非常活跃,社区人数稳步增长。这里简单回顾一下我知道的 Go 社区中的一些人和事。
Fango
如果在 2010 年关注 Go 语言,肯定会听到 Fango 的名字。Fango 是来自新加坡的 Go 语言爱好者,在 Go 语言刚面世不久他就写了第一本(很可能是唯一一本)以 Go 语言为题材的小说《胡文·Go》,然后他还出版了第一本 Go 语言中文教材《Go 语言·云动力》。感谢 Fango 给大家带来的精彩的 Go 语言故事。
许式伟和七牛云
七牛是国内第一家大面积采用 Go 语言开发的公司,时间还在 Go1.0 正式发布之前。许式伟也是大中华第一个知名的 Go 语言布道师。许式伟和七牛云在 2012 年也出版了一本《Go 语言编程》教程,和 Fango 的图书可能只差了一个多月的时间,编辑都是杨海铃老师。其后七牛还有多本 Go 语言相关的专著或译著,可以说在 2015 年之前,许式伟和七牛云团队绝对是国内 Go 语言社区推广的主力。
笔者也在第一时间拜读了《Go 语言编程》一书,对其中如何实现接口和 Goroutine 调度的模拟依然印象深刻。感谢许式伟当时赠送的签名版本《Go 语言编程》,同时也感谢为我新出的《Go 语言高级编程》写序,谢谢许大!
Astaxie 和 GopherChina 大会
对谢大最早的印象是在 2012 年前后,当时他开了一个免费的《Go Web 编程》图书,当前 QQ 群中很多小伙伴都参与审校(比如四月份平民、边江和 Oling Cat 等)。Go Web 编程是大家比较关注的方向,书中不仅仅讲到了 ORM 的实现,还讲到了 beedb 等组件。而 beedb 等这些组件最早演化成了 Beego 框架。根据前一段时间 JetBrains 展开的一个调查,Beego 是 Go 语言三大流行的 Web 框架之一。
然后到了 2015 年,谢大正式开启 GopherChina 大会的历程。我虽然因为其它事情没有现场参与,但是也预定了第一节 GopherChina 大会的会衫。然后在 2018 年终于以讲师身份参加了上海的 GopherChina 大会,跟大家分享了 CGO 方向的技术,同时第一次见到谢大本尊。感谢谢大的 GopherChina 大会和《Go Web 编程》!
其他人和项目
此外还有很多大家耳熟能详的 Go 爱好者,比如《Learning Go》和 Go Tour 的中文翻译者星星,创建了 gogs 的无闻,一种在翻译 Go 官方文档的 Oling Cat,雨痕的《Go 语言学习笔记》对 Go 源码深度的解读,创建了 GoHackers 的郝林等等。此外由国内的 PingCAP 公司主导开发的开源 TiDB 分布式数据库也是一个极为著名的项目。感谢 Go 中国社区这些朋友和项目,是大家的努力带来了 Go 语言在国内的繁荣。
向 Go 语言学习
候杰老师曾经说过:勿在浮沙筑高台。而中国互联网公司的繁荣更多是在业务层面,底层的基石软件几乎没有一个是中国所创造。作为一个严肃的软件开发人员,我们需要向 Go 语言学习,继续扎实掌握底层的理论基础,不能只聚焦于业务层面,否则下次中美贸易战的时候依然要被西方卡脖子。
经过这么多年发展,中国的软件行业已经非常繁荣和成熟,同时很多软件开发人员也开始进入 35 岁的中年门槛。其实 35 岁正是软件开发人员第二次职业生涯的开始,是开始形成自我创造力的时候。但是某些资本家短视的 996 或 007 等急功近利的福报观点正导致中国软件人员过早进入未创新而衰的阶段。中国的软件工程师不应该是码农、更不是码畜牧,我们虽然不会喊口号但是始终在默默前行。
目前中国已经有大量的软件开发人员有能力参与基础软件的设计和开发,正因为这一波脚踏实地程序开发人员的努力,我相信在下个十年我们可以 Go 得更远。
作者:柴树杉,国内第一批 Go 语言爱好者,Go 代码贡献者,同时也是《Go 语言高级编程》和《WebAssembly 标准入门》的作者。Github 账号为 chai2010。