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和网站的重新设计工作。
2019年是对Go语言发展极其重要的一年,今年8月将发布Go1.13,而这个版本将正式重启Go语言语法的进化,向着Go2前进。
总的来说,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的目标代码。
有疑问加站长微信联系(非本文作者)