解读 2018之Go语言篇(上):为什么Go语言越来越热?

2018年接近尾声,InfoQ 策划了“解读 2018”年终技术盘点系列文章,希望能够给读者清晰地梳理出重要技术领域在这一年来的发展和变化。本篇文章是Go语言2018年终盘点,分为上下两篇,客观、深入分析2018年Go语言的技术发展现状,同时对明年可能的发展情况进行预测和展望。

今年真可谓是不平静的一年,前有人工智能国家级战略的发布,行业已经在大跨步的挺进,但人才缺口每天都在扩大;后有区块链技术从爆发式增长到大幅回落,无数程序员蜂拥而至,又在现如今变得手足无措。

那么,Go语言在2018年这一年发展得又如何呢?它的下一步又将会怎样?且听笔者细细道来。

首先,笔者要说的是,在TIOBE于2018年11月份公布的编程语言排行榜中,Go语言已然挤到了前10的位置。虽然这与去年同期的第14位看起来相差不大,但却是一个里程碑式的进步。

图1:TIOBE Index for Nov 2018

从Google Trends提供的流行趋势统计来看,在过去的12个月里,Go语言的流行也是持续升温的。

图2: Google Trends - Golang热度随时间变化的趋势

这种升温虽然并不算快,但是很持久。这对编程语言的生态环境和人才的发展是非常有利的。

此外,完全不出乎我们的意料:中国依然是Go语言爱好者最多的国家,没有之一。

图3: Google Trends - Golang按区域显示的搜索热度

具有讽刺意味的是,作为Go语言诞生地的美国,仅排在了第15位。我们对先进技术和前沿科技的热衷绝对是不输他国的。下面,让我们再把尺度缩小到城市级别。

图4 :Google Trends - Golang按区域显示的搜索热度(城市)

显然,在我国,北京、深圳、上海这三个城市聚集了非常多的Go语言程序员和工程师。尤其是北京,简直是Go语言爱好者的圣地啊!

至于北京博得头筹的原因,据笔者观察,首先肯定是:在北京的互联网公司很多,起码明显多于其他的一、二线城市。Go语言如今在互联网公司中非常流行,即使有的公司高层并没有批准大规模地使用Go语言,但是工程师们都在做积极的尝试。

其次,北京做云计算的公司很多,不论是面向市场的公有云还是自建自用的私有云。说到云计算,我们就不得不提及开放平台技术、容器技术、集群管理技术,以及现在很火热的微服务(Microservices)和Serverless技术,等等。而这些,恰恰都是Go语言的专长。在这些方面,有很多成熟的基于Go语言的解决方案可供选择。

再次,北京的高科技创业公司非常多。他们往往没有历史包袱、勇于创造和尝试。在做技术选型的时候,他们也更容易选择Go语言。因为,Go语言既拥有编译型编程语言固有的高运行效率,又具有解释型编程语言常有的高开发效率。而且,Go语言还不像有些编程语言那样时不时地出现内斗、分裂等混乱情况,当然也没有无良的技术持有者吵闹着要对编程语言的商用进行收费。

Go语言在语言规范的发展、版本的迭代和开发者生态的建设方面都非常的稳定,并有着良好的包容性和兼容性。保持简单、面向契约和利于协作是Go语言最突出的设计哲学。无论是做软件原型,还是用于小团队作战,又或是进行大规模的研发,Go语言都会是很不错的选择。

最后,很多喜爱Go语言、致力于推广Go语言技术的个人开发者、技术团队、互联网公司以及知识服务厂商也都在北京。这都直接或间接地导致了Go语言在这座城市的流行。

好了,到这里,笔者相信你已经对Go语言在中国的流行有了一定的了解。下面,我们再来说说Go语言在2018年具体都有哪些进展。

首先说一下,关于Go语言在2018年之前的具体进展,笔者推荐你去看这几篇同系列文章,如下:

语法和平台

Go语言官方团队在2018年2月正式发布它的1.10版本。不同于其他很多被称为版本帝的编程语言,到了这样一个版本号10,Go 1在语言规范方面已经几乎没有什么改动了,一些语法上的小小增强也并不值得我们特别关注。而在2018年8月发布的Go 1.1更是没有任何语言规范方面的变动。

Go语言对于本身的向后兼容性保持得非常好,高版本对低版本中的语言语法、工具和标准库都不会有任何破坏。然而,Go语言在其支持的操作系统方面还是很大刀阔斧的。这体现在,Go 1.10不再支持10.3以下版本的FreeBSD和8.0以下版本的NetBSD。并且,这个版本也是支持OpenBSD 6.0、OS X 10.9以及Windows XP和 Windows Vista的最后一个版本。**在这些操作系统之上编写或运行Go语言程序的开发者们要注意。

环境和工具

使用过Go语言的开发者们都知道,当把Go语言的预编译包解压到某个目录后,我们还需要至少设置两个环境变量——GOROOT和GOPATH。前者代表直接包含Go语言本身的那个目录路径,而后者则用于指定可放置第三方库和自有代码的工作区(或者说工作目录)的路径。

一个好消息是,自Go语言的1.10版本起,GOROOT这个环境变量就没有必要设置了。如果我们不设置它,那么Go的标准工具会尝试以自身所在的目录为基础,自动地推断出GOROOT应该指向的目录路径。

另外,从这个版本开始,我们可以自行地设定Go语言的临时目录路径了,设定的途径是设置环境变量GOTMPDIR。Go语言的临时目录主要用于存放Go工具在编译或测试程序时产生的各种临时文件。在这之前,这些临时文件都会被存放到固定的地方,此地的具体路径会根据操作系统的不同而不同,一般会位于操作系统的临时目录的某个子目录下。自定义这个目录的好处在于,可以让我们方便地观察编译过程,并查看编译或测试的中间结果。

说到编译,笔者一定要提一下1.10版本的另一项改进,这与go build命令有关。以前,如果我们要强行地重新构建所有相关的代码包,那么就需要在运行这个命令的时候追加标记“-a”。而现在,我们无需这样做了。go build命令会根据源码文件内容、构建标记和编译元数据,自动地决定什么时候应该重新构建那些代码包。这项工作再也不需要人工干预了。

与此项改进相关的变化是,go build命令现在总是会把最近的构建结果缓存起来,以便在将来的构建中重用。我们可以通过运行go env GOCACHE命令来查看缓存目录的路径。缓存的数据总是能够正确地反映出当时的源码文件、构建环境和编译器选项等的真实情况。一旦有任何变动,缓存数据就会失效,go build命令就会再次真正地执行构建。因此,我们并不用担心缓存数据体现的不是实时的结果。实际上,这正是上述改进能够有效的主要原因。go build命令会定期地删除最近未使用的缓存数据,但如果你想手动删除所有的缓存数据,运行一下go clean -cache命令就好了。

顺便说一下,对于测试成功的结果,go命令也是会缓存的。运行go clean -testcache命令将会删除掉所有的测试结果缓存。不过别担心,这样做肯定不会删除任何的构建结果缓存,它们是两码事。

此外,设置环境变量GODEBUG的值也可以稍稍地改变go命令的缓存行为。比如,设置值为gocacheverify=1将会导致go命令绕过任何的缓存数据,而真正地执行操作并重新生成所有结果,然后再去检查新的结果与现有的缓存数据是否一致。

再来说go install命令。现在,go install命令在默认情况下只会去安装我们明确指定的那些代码包。这些代码包依赖的那些包并不会被安装。这同样得益于构建结果缓存,它可以使安装的速度得到明显的提升。如果你想要强制地安装依赖包,那么请在运行命令的时候追加“-i”标记。

程序测试

前面我们说过了,测试成功的结果也会被缓存。如果go test命令确定可以使用被缓存的结果,那么它打印出的内容也会出自于缓存。这时,被打印的内容中会包含“(cached)”字样。

另外,go test命令现在会自动地运行go vet命令,以便在真正运行测试之前识别出一些程序编写方面的问题。我们都知道,go vet命令用于对Go语言源码进行静态检查,并报告已发现的可疑问题。这些问题一般都是符合语法规则的,因此编译器无法查出它们。但是,它们很有可能代表了对某些程序实体(或者说API)的错误使用。虽然go vet命令有时候并不能保证它报告的每一个问题都是真正的问题,但它却可以给予我们一份重要的参考,以便让我们在编程的过程中小心行事。

与Go语言提供的很多高级功能一样,我们也可以阻止go test命令自动运行go vet命令,这需要在运行前者的时候追加“-vet=off”这个标记。

最后,关于go test命令,还有两个值得注意的新标记——“-failfast”和“-json”。顾名思义,“-failfast”标记可以让go test命令一旦发现有测试失败的情况就立即忽略掉剩余的测试并终止运行。不过要注意,如果存在与失败的测试并发进行的测试的话,那么后者还是会继续运行直至完成的。“-json”标记对于程序测试的自动化大有裨益。它会让go test命令产生JSON格式的测试报告,这使得其他程序很容易读入和处理。

程序文档

关于程序文档,只有一点需要我们注意。**Go 1.11是godoc命令支持命令行接口的最后一个版本。**在未来的版本中,我们运行godoc命令的时候,它会启动一个Web服务器,以便让我们直接进入图形化界面进行文档查询。

程序性能分析

现在,runtime/pprof代码包中的Lookup函数已经支持了更加多样的参数值。这就意味着,Go语言的程序性能分析现在可以生成和解读更多视角下的分析报告了。我们可以把这样的分析报告包含的内容叫做程序性能概要信息(简称概要信息),并把存储这些分析报告的文件叫做概要文件。

Lookup函数可以生成的概要信息目前共有6种。这6种概要信息分别由字符串类型的参数值goroutineheapallocsthreadcreateblockmutex代表。下面是它们代表的含义:

  • goroutine:收集当前正在使用的所有goroutine的堆栈跟踪信息。
  • heap:收集与堆内存的分配和释放有关的采样信息,默认以在用空间(inuse_space)的视角呈现。
  • allocs:同样收集与堆内存的分配和释放有关的采样信息,但默认以已分配空间(alloc_space)的视角呈现。
  • threadcreate:收集一些特定的堆栈跟踪信息,其中的调用链上的代码都导致了新的操作系统线程的产生。
  • block:收集因争用同步原语而被阻塞的那些代码的堆栈跟踪信息。
  • mutex:曾经作为同步原语持有者的那些代码的堆栈跟踪信息。

这里所说的同步原语,指的是存在于Go语言运行时系统内部的一种底层同步工具,或者说一种同步机制。它是直接面向内存地址的,并以异步信号量和原子操作作为实现手段。我们已经熟知的通道、互斥锁、条件变量、“WaitGroup”以及Go语言运行时系统本身,都会利用它来实现自己的功能。

另外,在用空间和已分配空间的区别是,前者指的是已经分配但还没有被回收的空间,而后者只关注分配出的空间,不论它们是否已经被回收。

注意,如果我们在运行go test命令的时候追加了标记“-memprofile”,那么该命令会通过底层的API以allocs为视角生成概要信息和概要文件。这相当于对从测试开始时的所有已分配字节进行记录,包含已经被垃圾回收器收回的那些字节。在Go 1.11版本之前,go test命令在这种情况下采用的是heap视角。

最后,go tool pprof工具已经可以正确地单独读取和处理所有种类的概要文件了。这得益于,从Go 1.10版本开始,blockmutex视角下的概要信息已经完善。在这之前,我们使用go tool pprof查阅这两种概要文件的时候,还不得不同时指定相应程序的二进制文件。

运行时系统

需要特别注意,runtime代码包中的LockOSThread函数和UnlockOSThread函数的行为已经发生了变化。我们都知道,前一个函数的功能是将当前的goroutine与那一时刻正在承载这个goroutine运行的操作系统线程进行绑定。在绑定之后,这个goroutine就只能由该操作系统线程运行了,反之,该操作系统线程也只能运行这一个goroutine了。显而易见,runtime. UnlockOSThread函数的功能是解除上述绑定关系。当然了,这两个函数都只能作用于它们被调用时所在的那个goroutine。

以前,runtime. LockOSThread函数是幂等的。也就是说,无论我们在同一个goroutine中调用了它多少次,都只相当于调用了一次。另一方面,只要我们调用一次runtime. UnlockOSThread函数,就总是能够解除针对于当前goroutine的这种绑定。

但是,从Go语言的1.10版本开始,在我们想要完全解除绑定的时候,可能就需要调用多次runtime. UnlockOSThread函数才能够实现了。至于具体需要调用多少次完全取决于,当初在同一个goroutine中调用runtime. LockOSThread函数的次数。换句话说,只有进行相同次数的函数调用,才能让当前goroutine与某个操作系统线程之间的绑定关系完全解除。我们可以把现在的这种对应关系理解为是基于嵌套的,可以想象一下:当初包装了多少层纸箱,现在就要拆开多少层纸箱。

其实一直以来,有很多第三方Go语言库的作者都误以为对于这两个函数的调用就是基于嵌套关系的。不过无论怎样,我们现在都应该仔细检查代码并小心的应对了。

笔者认为,如果你确实需要进行这种绑定,那么就应该基于这两个函数封装一个数据结构。在这个数据结构中,至少应该包含一个用于记录调用runtime. LockOSThread函数次数的字段,以方便后续的解绑操作。

在2018年,对于Go语言的运行时系统来说,我们可以轻易感知到的变化基本上只有这一个。不过,非常多的改进和优化都在悄无声息的进行着,有的已经完成了,而有的还在进展之中。已完成的改进如:在通常情况下,我们传递给runtime.GOMAXPROCS函数的参数值已经不再受限了,只要它在int32类型可容纳的范围之内就可以。

标准库

在Go语言的1.10和1.11这两个版本中,官方团队与社区开发者们一起对标准库做了大量的改进。可喜可贺,社区开发者对Go语言的贡献次数现在已经超过官方团队了!

由于这方面的改进繁多,也由于笔者在新近发布的极客时间专栏《Go语言核心36讲》中已经详细讲解了不少,所以这里就不再赘述了。

两个新实验

我们再来说说Go 1.11的两个新实验吧,一个是对WebAssembly的实验性支持,另一个是推出由dep和vgo演化而来的依赖管理机制和新概念module。

按照官方的描述,WebAssembly(缩写为WASM)是一种二进制指令格式,它针对的是以堆栈为基础的虚拟机。WASM有很好的可移植性,以便让C++、Golang、Rust等高级编程语言来操控它,并有能力部署到Web程序上。

用普通话来说,WASM提供了一种途径,可以让我们用后端编程语言直接去编写Web页面中的逻辑。在Go 1.11中,我们可以很轻易地把Go语言源码文件转换为WASM格式的文件,然后在Web页面中通过寥寥几行JavaScript代码引用这个文件并把其中的逻辑发布到页面上。WASM的1.0版本现在已经支持了绝大多数的主流网络浏览器,比如:Chrome、Firefox、Safari等。如果想了解具体的玩法,你可以参看这个wiki页面

笔者对Go语言官方的这种探索性实验一直都持赞成的态度,不论是前些年的移动端(Android和iOS)方向,还是今年的Web端(WASM)方向。不过,笔者依然觉得Go语言的优势在服务端,现在很明显,而且在可预见的未来也应该是如此。所以,对于这些多端探索,笔者建议大家“保持关注,积极试验,但不要偏移重心”。

相比之下,笔者倒是更加看好Go语言新放出的依赖管理机制。Go语言爱好者们都知道,Go语言在这方面一直是缺失的。虽然目前存在几个不错的第三方解决方案,但是没有一个是可以脱颖而出的,同时官方也一直没有给出一个统一的标准。

经过了一段时间的试验和演化,Go语言官方的依赖管理机制终于脱胎于dep和vgo。虽然其间存在一些摩擦和风波,但是结果终归是积极的。

在Go语言新的依赖管理机制中,module是一个非常重要的概念。简单来说,module象征着由某个Go语言代码包以及它依赖的代码包共同组成的一个独立单元。这里的Go语言代码包和它依赖的那些代码包都是版本化的。一个module的根目录下总是直接存有一个名为go.mod的文件。这个文件中会包含当前module的路径,以及它依赖的那些module的路径和版本号。如此一来,对于每一个版本的module,它依赖的所有代码都会被固化下来。这对于后续的版本管理和module重建来说都是重要的基础。详情可以参看这里的wiki页面

不过,不要忘了,Go 1.11中包含的这个依赖管理机制是实验性的。其中的任何部分都有可能由于社区的反馈和官方的改进而变化。所以,你在正式使用它之前一定要考虑到后续可能存在的变更成本。虽然如此,笔者仍然会鼓励广大开发者们去积极使用和反馈。想象一下maven对于Java世界的重要性吧。笔者相信,我们心目中的Go项目依赖管理机制已经离此不远了。

参考文献

[1] Go 1.10 is released: https://blog.golang.org/go1.10

[2] Go 1.11 is released: https://blog.golang.org/go1.11

[3] Diagnostics: https://golang.google.cn/doc/diagnostics.html

[4] WebAssembly: https://github.com/golang/go/wiki/WebAssembly

[5] Modules: https://github.com/golang/go/wiki/Modules

[6] Go 1.12 Release Notes(DRAFT): https://tip.golang.org/doc/go1.12

[7] Nine years of Go: https://blog.golang.org/9years

[8] Toward Go 2: https://blog.golang.org/toward-go2

[9] Go 2 Draft Designs: https://go.googlesource.com/proposal/+/master/design/go2draft.md


作者简介
郝林,国内知名的Go语言技术布道者,GoHackers技术社群的发起人和组织者。他也是极客时间专栏《Go语言核心36讲》的作者,以及图灵原创图书《Go并发编程实战》的作者。他曾在轻松筹任大数据负责人,同时负责大数据部门和主站的后端技术团队。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章