Go语言2017年终总结

2018 年 1 月 5 日 聊聊架构 郝林
作者 | 郝林
编辑 | 雨多田光
写在前面

Google 的 Go 语言团队会在 2018 年初宣布 Go 1.10 正式版的发布,“10 ”这个二位数也预示着 Go 语言的进一步成熟。据说,Go 2 也被提上了日程,Go 2 将会是大神们对 Go 语言的一次彻底反思和改进。虽然现在细节还没有被暴露出来,但是这已经足以让 Gopher(Go 语言爱好者)们激动不已了。会有泛型支持吗?GC 会变革吗?详细调参会可行吗?各种猜测已经在各个论坛和群组里层出不穷了。

不过,饭要一口一口吃,肌肉要一点一点练。在憧憬未来之前,先让我们看看 Go 语言在 2017 年的表现。

首先,根据 Google Trends 的统计结果,我们可以看到 Go 语言在过去一年中的流行程度是稳中有升。

图 1 Go 语言在 2017 年的流行趋势

初看起来,Go 语言在 2017 年表现得比较平淡,但是,让我们把时间线拉长。

图 2 Go 语言在过去 5 年间的流行趋势

你一定看到了,Go 语言在 2017 年的“上升”是对近年来的一种延续。有些编程语言会随着其擅长领域的爆红或遭冷而大起大落,但是 Go 语言不会。这就像它朴素的编程风格一样,它的使用范围很广,我几乎可以在任何场景下使用它。

Go 语言的适用范围一直在不断地扩大,经过广大开发者的共同努力,它已开始涉足在当前大热的数据科学和机器学习领域。虽然还只是开始,但是就像我在《Go 并发编程实战》第 2 版的扉页中说的那样,我“深信 Go 语言在人工智能时代和机器人时代也能大放异彩”。

更令人欣慰的是,中国的开发者对于 Go 语言的流行起着至关重要的作用。在过去的一年里,我们的热衷和贡献依然领跑全世界!

图 3 Go 语言在 2017 年的流行区域热图

回顾

我在前年和去年分别写过两篇相关的文章——《解读 2015 之 Golang 篇:Golang 的全迸发时代》和《解读 2016 之 Golang 篇:极速提升,逐步超越》。大家可以把它们当做一个系列的记录参看。

类型别名

类型别名(type aliases)原本是要在 Go 1.8 发布时推出的,但是由于一些争议和实现上的问题,Go 团队把它推迟到了 Go 1.9。

这一特性其实是为开发者们的代码库重构准备的。代码重构是对代码的重新组织,以及这种重组与代码包之间的关系的重新思考和修改过程。代码重构的原因可能是代码的拆分、命名优化或者依赖关系清理,等等。不论原因是什么,我们的目的都是让代码变得更清楚、更易于使用,以及更容易扩展。

也许你有过这样的经历:在进行(范围比较广的)代码重构的过程中,原有代码已经改变。甚至,当你完成重构并想把代码合并回去的时候,原有代码已经面目全非。合并代码有时候比重构代码更加困难。

这就引出了一个比较先进的重构方法——渐进式代码重构。也就是说,同时计划重构的终极目标和阶段性目标。在达到完备状态之前,设置几个易实施并且可用的中间状态。纵观 Go 语言的代码更新和版本升级过程,我们不难发现 Go 团队对这种方法的合理运用。

然而,在渐进式代码重构的过程中我们也可能会遇到问题。例如,当你想把一个允许包外代码访问的类型迁移到另外一个包中时,该怎么做?简单来说,应该分为三步:

  • 1)在目的包中声明一个名称和功能都相同的新类型。

  • 2)检查所有可能引用原类型的地方,并把那些引用都指向新类型。

  • 3)删除原包中的那个类型。

如果这个类型只被由你掌控的程序引用,那么这种方式当然没问题。但是,如果该类型所在的是一个已被广泛依赖的底层代码包,那我劝你还是不要执行第 3 步了,除非你冒着被口水淹死的风险发布程序不兼容声明。可是不执行第 3 步就等于任由废弃代码在程序中蔓延,这也是很恶心的一件事。

讲了这么多,我要说的重点是:Go 的类型别名就是为我们解决这类两难问题的。在类型别名真正问世之前,Go 团队自己在做上述重构时都不得不用上一些特殊的、脆弱的手段。

若使用类型别名的话,应该怎么做?如果我们要把 oldpkg.OldType 类型迁移到 newpkg 代码包中并将其改名为 NewType,那么最少用 2 步就可以完成:

  • 1)声明 newpkg.NewType 类型。

  • 2)把 oldpkg.OldType 类型的声明改为:

    package oldpkg
    type OldType = newpkg.NewType

新的 oldpkg.OldType 类型声明可以使它与 newpkg.NewType 完全等价,并可以实现互换。也就是说,如果一个函数有一个 oldpkg.OldType 类型的参数声明,那么该函数就可以接受一个 newpkg.NewType 类型的参数值。

当然,为了不留废弃代码你仍然需要在某个时间删除掉 oldpkg.OldType。但是类型别名给了你和其他使用 oldpkg.OldType 的开发者一个可以游刃有余的必要条件,你们为重构代码而设置的中间状态都可以轻松达成。

举个例子,你在按照上述 2 步迁移完类型之后,可以马上把自己写的某个函数的声明由

package handler
import oldpkg
func HandleXXX(obj oldpkg.OldType){}

改为

package handler
import newpkg
func HandleXXX(obj newpkg.NewType){}

然而,在调用该函数的(其他人写的)程序那里,却可以不用做任何修改(虽然最后可能要修改)。代码 handler.HandleXXX(oldTypeVar) 仍然有效,其中的 oldTypeVar 是 oldpkg.OldType 类型的值。这就相当于可以让各方按照自己的节奏重构代码了。

再后面的情景可能是:你在你的代码包还是 1.0 版本的时候就发布声明说 oldpkg.OldType 类型即将在 2.0 版本中删除,而当你在开发 2.0 版本时,心安理得地删掉 oldpkg.OldType。

实际上,类型别名只是 Go 团队为了让我们顺利实施大规模 Go 软件工程的举措之一。他们一直在致力于帮助开发者们高效地编写 Go 代码和利用 Go 代码包,并避免因不经意的重复造轮子而导致的代码膨胀。如果你真正用过 Go 语言,那么也一定能体会到它在代码依赖方面展现出的规范性和严谨性。

sync.Map

广大 gopher 们又迎来了一个提供并发安全性的高级数据结构——sync.Map。这个数据结构提供了一些常用的键、值存取操作方法,并保证了这些操作的原子性。同时,它也保证了存取的性能——算法复杂度依旧是 O(1) 。相信很多人已经期盼了很久。不过请注意,它是 Go 语言标准库中的一员,而不是语言层面的东西。也正因为这一点,Go 对它的键类型和值类型并无程序编译期的类型检查,我们只能在程序运行期自行保证键、值类型的正确。

如果你一直关注 Go 语言,可能已经猜到它就是之前一直蛰伏在 golang.org/x/sync/syncmap 包中的那个 struct。现在它被纳入了标准库,并将会获得更多底层优化的机会。

在 sync.Map 问世之前,我们如果需要并发安全的字典结构,那么就需要自行搭建。这其实也不是麻烦事,使用 sync.Mutex(互斥锁)或 sync.RWMutex(读写锁)再加上原生的数据类型 map 就可以轻松办到。GitHub 网站上就有很多库提供了类似的数据结构。我在《Go 并发编程实战》第 2 版中也提供了一个实现较完整的并发安全字典,它的性能比同类的第三方数据结构还要好一些,因为它在很大程度上有效地避免了对锁的依赖。

大家应该都知道,使用锁就意味着要把一些并发的操作强制串行化,这对程序的性能是有很大的负面影响的,尤其是在有多个 CPU 核心的情况下。因此我们常说,能用原子操作就不要用锁。可惜前者只对一些基本数据结构提供支持。

不管是哪一种操作,它们在多个 CPU 核心面前都是相对低效的。因为只要是对共享状态(比如多个线程都可见的同一个变量)的存取就会涉及到状态的同步。在 CPU 层面,这种同步就是 cache 级别的,也可称之为 cache contention。你一定听说过 CPU 的 L1 cache 和 L2 cache。

举个例子,如果在不同 CPU 中运行的线程同时在操作同一个锁(更确切地说是锁中的计数变量,具体可参看 sync.Mutex 的源码),那么它们会争相声明存在于自己的 cache 中的那个变量的值是唯一有效的(或者说是最新的)。一旦有一个线程声明成功,那么运行于其他 CPU 核心中的线程再想存取相同的变量就必须先从前者那里做同步。这个同步的耗时在 CPU 层面是很可观的。

那么,sync.Map 帮我们解决上述问题了吗?很遗憾,答案是没有完全解决。实际上,这根本就不是在应用层面(甚至操作系统层面)可以完全解决的问题。我们只能经过一轮又一轮的优化获得更高的性能,或者说逐渐逼近最高性能。

sync.Map 在内部使用了大量的原子操作存取键和值, 并利用两个 map 作为存储介质。

其中一个 map(字段 read)可被视作一个快照,这个快照也可被称为只读字典,它保存了在前一个操作周期结束时 sync.Map 值中包含的所有键值对。虽然其中的键所对应的值都可以被更改,但是键绝不会有增减。因此,这里的“只读”是对其中的键的集合而言的。如此一来,对只读字典的操作无需使用锁。实际上,在 sync.Map 的增、删、改、查方法中会首先尝试操作只读字典。

另一个 map(字段 dirty)中存有最新的键值对的集合,它也可被称为脏字典。新的键值对会首先被存储到该字典中。sync.Map 中所有的增、删、改、查方法在操作脏字典时都需要在锁(字段 mu)的保护下进行。

在达到当前操作周期边界的时候,sync.Map 会把脏字典提升为只读字典,并把存储脏字典的字段设置为 nil。当再有新的键值对存入时,sync.Map 会对脏字典进行重新初始化,并把只读字典(前一个操作周期中的脏字典)中的所有键值对反灌入脏字典,最后把新的键值对也加入其中。在新的操作周期中,(新的)只读字典和(新的)脏字典依然分别代表着键值对集合的快照和最新版本。如此一来,通过两个字典之间的周期性同步,sync.Map 就实现了键值对操作的“快路径”和“慢路径”。“快路径”就意味着无锁化操作,而“慢路径”仅在“快路径”不通时才会被考虑。

顺便说一句,sync.Map 在判断当前操作周期的边界达到与否时依据的是一个记录着“在只读字典中未找到被查询键”这种情况的发生次数的计数值(字段 misses)。一旦这个计数值等于脏字典的长度,就意味着一个操作周期的结束。显然,操作周期会随着脏字典的增大而变长。

图 4 从键值对的流转方式看 sync.Map

上图从另外一个角度展现了 sync.Map 存取键值对的方式。

总体上讲,sync.Map 的内部实现就是如此。如果你想探究细节可以查看 Go 语言标准库代码包 sync 中的 map.go 文件。

通过对 sync.Map 的实现的理解,我们就可以分析出它的优势和劣势。显然,sync.Map 更适合于键的集合相对固定的场景,这时只有一些必要的原子操作会对性能有轻微的影响。更具体地讲,如果所有的键值对都在初始化 sync.Map 值时加入,之后仅有键值对的读取和更新而没有添加,那么 sync.Map 就会发挥出很高的性能。当然,如果在多个线程中同时对同一个键的值进行更新,那么还会存在由原子操作引发的竞争,这也会涉及到 cache contention。

另一方面,如果在使用过程中有非常多的新键存入的话,那么在极端情况下它的性能很可能会回退至 map + sync.Mutex/sync.RWMutex 的水平,甚至还会不如后者,别忘了其中还有很多原子操作。另外,请注意,sync.Map 所占用的空间比 map 要多,至于多多少,还要看实际的键值对获取情况。

以上就是我对 sync.Map 的简单剖析,希望可供大家在选择字典的并发安全策略时参考。

其他值得注意的改进

并行编译

Go 1.9 默认会并行地编译你的代码,不过前提是你的机器上有多个 CPU 核心。其实在这之前并行编译也是存在的,只不过那时的粒度是代码包,也就是说,不同的代码包可被并行地编译。但是 Go 1.9 的并行编译粒度是函数级别的,这显然可以使编译更加迅捷。

关于 vendor 目录的处理

之前我们执行 go build ./... 命令的时候,当前目录内的 vendor 目录也会被编译,但是现在必须输入 go build ./vendor/... 才可以。对于其他的 Go 标准命令也是如此。我就为此烦恼过,所以很高兴看到这样的变化。请想象一下,当你在执行 go test ./... 的时候,Go 也会测试你依赖的那一坨代码包。在很多时候这根本就没有必要,白白浪费时间。

关于 GC

我们都知道,Go 的 GC 是并发的,但这只限于自动 GC。当我们手动触发 GC 时,Go 运行时系统依然会“Stop the world”(或者说停止内部调度)。值得庆幸的是,Go 1.9 为我们带来了对手动 GC 的并发支持!这涉及到了 runtime.GC、debug.SetGCPercent 和 debug.FreeOSMemory 函数。不过要注意,调用这些函数的 goroutine 是会被阻塞的,直到 GC 完成。

除此之外,GC 的性能又得到了进一步提升,尤其是在拥有庞大(大于 50GB)的堆内存的情况下。

单调的时间度量

在 Go 1.9 之前,标准库代码包 time 中的一些函数是通过读取计算机的系统时钟来实现功能的。这样做的好处是总与系统保持一致,而坏处是一旦系统时钟被篡改,它们也会无脑地跟着变更。这可能会让使用它们的 Go 程序产生错误,尤其是我们在依赖时间做一些任务的时候。请想象一下,千年虫或者闰秒调整那样的问题再次出现时会怎样。因此,Go 必须优雅地应对系统时钟重置。这在 Go 1.9 中得到了解决。

现在,包括 runtime、time、context 和 net 在内的代码包都使用了单调的时间度量。在必要时,系统时钟重置导致的时间度量错误会被自动修正,我们不会再为此困扰了。

除了上述的这些可喜的改进之外,Go 标准库也被大范围地更新了。比如,新的用于处理位操作的 math/bits 包;又比如,testing 包对帮助函数更加友好;还比如,runtime/pprof 包中的诸多改进……

在开始撰写本文之前,Go 1.9.2 早已发布。在我的 Go 程序研发团队中有个规定:通常情况下,当小版本(比如 1.9)的第二个维护版本出来之后,我们就开始更新各种测试环境甚至生产环境上 Go 的版本。当然,我们的评估和试用在小版本的第一个版本发布时就开始了。这既可以避免因过于激进而踩坑,又可以尽早享用 Go 版本升级的红利。在这里供大家参考。

展望
Go 1.10+

先说说离我们最近的 Go 1.10。该版本的 beta 版已经发布,它又带来了众多改进,比如:更宽松的语法、更智能的标准命令、更完善的 Go 汇编支持、无限的最大 P 数量(GOMAXPROCS)设置,以及一如既往的性能提升和标准库改进。

注意,有两个标准库代码包的变更可能会导致你代码的必要修改。一个是 bytes 包,涉及到 Fields、FieldsFunc、Split 和 SplitAfter 函数。另一个是 net/url 包,主要涉及到 ResolveReference 函数。后者不会再想当然地去掉 URL 路径中的“多余”斜杠。这样可以避免 http.Client 在某些情况下的重定向错误,同时也是为了符合 RFC 3986 协议。

至于以上变更的细节,请大家参看 Go 1.10 的 release notes。

在 Go 即将满 10 岁之际,在 Gophercon 2017 大会上,Go 团队终于把 Go 2 的事情郑重地摆上了桌面。Go 语言的目标一直是帮助开发者们高效地完成现代软件的开发和部署。Go 2 的目标仍然如此。但更重要的是,Go 2 会修正 Go 1 中不利于实现前述目标的一些方面。据我个人推测,Go 2 的升级方式会介于 Java 和 Python 之间,同时在广大开发者可接受的范围内会更倾向于 Python。大家都知道,Java 的大版本升级过于保守,而 Python 的大版本升级则过于激进。

Go 2 可能会出现很大的变革,但就像 Java、Python 这些“白胡子”语言一样,大版本的升级必然会存在很多权衡和妥协,也必然会包含与 Go 1.x 的不兼容。到目前为止,Go 团队还没有公布任何细节。改革错误处理方式?增加不可变值?自定义泛型?一切都没有定数。但不论怎样,Go 团队会想尽一切办法让广大开发者平滑过渡到 Go 2 之上。让我们翘首期盼吧!

社区,永远的社区

国内使用 Go 语言的人数依然在不断增长,其增长程度已经让一些在线教育公司敏锐地嗅到了机会,他们已经把发展 Go 语言相关课程作为了 2018 年的重点之一。

2017 年我对国内社区的最大感触就是地方性的 Go 语言组织越来越多了。许多省市内的 gopher 们都自行组织起来,一起加强交流、共同增进技能。就拿 12 月 16 日在深圳举办的 meetup 来说,当场人数也超过了 100,现场的技术氛围也很浓郁。大家都在积极地互通有无,可喜可贺!现在各地方的中小型 Go 语言技术聚会基本上都可以达到这个规模了。

今年我在北京只组织了一场活动,有些遗憾,我在这里也检讨一下。

我去年呼吁各个使用 Go 语言的公司在 GitHub 上 Go 语言的 wiki(https://github.com/golang/go/wiki/GoUsers)中加入自己公司的主页链接。那时,China 那一栏下只有一家公司,如今增加到了 5 家。可是这仍然比我知道的公司少太多了。因此我在今年再呼吁一下,大家多向官方以及国际发声吧!这对我们是有好处的。

另外,我很久之前在 GitHub 上建立了一个国内卓越 Go 项目列表(https://github.com/GoHackers/awesome-go-China),也希望大家能把个人或者公司开源的项目加入其中。如此一来,我们就可以在进行 Go 框架和工具选型的时候有一个比较集中、方便的参考之地,同时也可以增进大家的交流。我们可以在 GitHub 上更有的放矢地为优秀项目贡献代码,另一方面,这也是吸引代码贡献者的另一个渠道。

从人才市场的方面看,国内招聘 Go 工程师的公司也越来越多了,就算在年底,各大 Go 语言微信群和 QQ 群里也能看到很多招聘启事。看来明年又是 Go 工程师们大展拳脚的一年。

我个人认为 Go 语言在当前以及可见的未来会更多地应用在如下几个热门领域中:

首先当然是云计算。Go 不仅擅长 Web 系统、API 服务等应用层软件的开发,也可以用来开发中间件甚至基础设施。云计算中几乎所有的软件都可以用 Go 语言来开发,而且很有优势。实际上,在这个领域,可以说 Go 语言已经成为大家的首选语言了。这部分得益于 Docker、Kubernetes 等项目的持续火爆。

其次是区块链。现在国内做区块链的公司有很多,这些公司也大都以 Go 语言为主力。虽然比特币(或者说加密数字货币)现在在国内的状态不容乐观,但是作为其核心技术的区块链却依然受到热捧。据我所知,很多 Go 工程师投身其中了,甚至有些国外的区块链创业公司已想在国内开设办公室了。顺便说一句,对区块链技术有兴趣的 gopher 可以去研究下 hyperledger 这个项目。

再次是数据科学和机器学习这两个领域。很多人会说“这都是 Python 的底盘啊,而且几乎不可动摇”,我要说的是,其实在这两个领域中 Python 也不是一家独大的,像 R、Julia、MATLAB、C++ 等语言都有自己的一席之地。对标 numpy,最近持续升温的 Gonum 很不错。另外,Tensorflow 框架也早早放出了 Go 语言版本的 API。当然我不是鼓吹大家在这些领域中使用 Go 语言,各个语言在不同领域都有各自的优势,重点是怎样提高生产力,但是我认为 Go 语言会在这些领域持续渗透并扩大优势。各位可以重点关注一下。

在这个面临着技术和商业大变革的时代,Go 语言是一项很好的技术资本,起码你应该让它作为你技术工具箱中的重要一员。如果你主攻后端技术但还不会 Go 语言,我建议你花几个小时入门一下,然后再花一些时间体会和理解它的编程哲学,我相信这对你的技术成长是很有好处的。

最后,如果你想融入一个国内的 Go 语言技术社区,不妨加入我发起的组织——GoHackers。它的前身是 Go 语言北京用户组(微信公众号:golang-beijing)。后者已经有 3 年左右的历史了。GoHackers 的微信公众号是“gohackers”,在开发者头条中的团队号是“GoHackers”。前者主要用于发布 Go 语言动向、国内活动预告以及相关招聘启事,后者主要用来分享优秀的国内外 Go 技术文章。

配套 repo

https://github.com/hyper0x/review2017-go

参考文献
  • Go 1.8 is released:

    https://blog.golang.org/go1.8

  • Go 1.9 is released:

    https://blog.golang.org/go1.9

  • Go 1.10 Release Notes(DRAFT):

    https://tip.golang.org/doc/go1.10

  • Toward Go 2:

    https://blog.golang.org/toward-go2

  • Eight years of Go:

    https://blog.golang.org/8years

  • Type Aliases(Proposal):

    https://github.com/golang/proposal/blob/master/design/18130-type-alias.md

  • An Overview of sync.Map:

    https://github.com/gophercon/2017-talks/blob/master/lightningtalks/BryanCMills-AnOverviewOfSyncMap/An%20Overview%20of%20sync.Map.pdf

  • Monotonic Elapsed Time Measurements in Go(Proposal):

    https://github.com/golang/proposal/blob/master/design/12914-monotonic.md

  • Diagnostics:

    https://tip.golang.org/doc/diagnostics.html

  • Codebase Refactoring (with help from Go):

    https://talks.golang.org/2016/refactor.article

作者介绍

郝林,从业近 13 年的互联网软件开发者。《Go 并发编程实战》的作者,该书已于 2017 年 4 月出版了第 2 版。技术社区 GoHackers 和 AIHackers 的发起人和组织者;微服务、数据挖掘和 DevOps 技术的积极实践者。现在某中型电商创业公司任技术总监。

More

聊聊架构2017下半年精选文章

如果潜心研究Netflix微服务技术多年,能学到什么?

其它

随着互联网业务的飞速发展,系统动辄要支持亿级流量压力,架构设计不断面临新的挑战。海量系统设计、容灾、健壮性,架构师要考虑多方面的需求做出权衡。不如来听听国内外知名互联网公司的架构师分享架构设计背后的挑战与问题解决之道。

QCon 北京 2018 目前 8 折报名中,立减 1360 元,有任何问题欢迎咨询购票经理 Hanna,电话:15110019061,微信:qcon-0410。

登录查看更多
2

相关内容

【Manning新书】现代Java实战,592页pdf
专知会员服务
99+阅读 · 2020年5月22日
【实用书】Python技术手册,第三版767页pdf
专知会员服务
234+阅读 · 2020年5月21日
《动手学深度学习》(Dive into Deep Learning)PyTorch实现
专知会员服务
119+阅读 · 2019年12月31日
2019年10月编程语言排行榜:前8名15年如一日
大数据技术
7+阅读 · 2019年10月13日
2018年游戏行业研究报告
行业研究报告
3+阅读 · 2019年4月20日
自然语言处理(NLP)知识结构总结
AI100
51+阅读 · 2018年8月17日
Python 杠上 Java、C/C++,赢面有几成?
CSDN
6+阅读 · 2018年4月12日
论机器翻译之浅薄
AI100
3+阅读 · 2018年4月7日
Python为啥这么牛?
Python程序员
3+阅读 · 2018年3月30日
快乐的迁移到 Python3
Python程序员
5+阅读 · 2018年3月25日
2017年深度学习总结:文本和语音应用
专知
3+阅读 · 2018年2月4日
Learning by Abstraction: The Neural State Machine
Arxiv
6+阅读 · 2019年7月11日
Neural Approaches to Conversational AI
Arxiv
8+阅读 · 2018年12月13日
The Matrix Calculus You Need For Deep Learning
Arxiv
12+阅读 · 2018年7月2日
Arxiv
6+阅读 · 2018年2月26日
Arxiv
3+阅读 · 2017年11月21日
VIP会员
相关资讯
2019年10月编程语言排行榜:前8名15年如一日
大数据技术
7+阅读 · 2019年10月13日
2018年游戏行业研究报告
行业研究报告
3+阅读 · 2019年4月20日
自然语言处理(NLP)知识结构总结
AI100
51+阅读 · 2018年8月17日
Python 杠上 Java、C/C++,赢面有几成?
CSDN
6+阅读 · 2018年4月12日
论机器翻译之浅薄
AI100
3+阅读 · 2018年4月7日
Python为啥这么牛?
Python程序员
3+阅读 · 2018年3月30日
快乐的迁移到 Python3
Python程序员
5+阅读 · 2018年3月25日
2017年深度学习总结:文本和语音应用
专知
3+阅读 · 2018年2月4日
Top
微信扫码咨询专知VIP会员