Go语言的2017年终总结

写在前面

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

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

首先,根据 Google Trends 的统计结果(https://trends.google.com/trends/explore?q=golang&hl=en-US),咱们能够看到 Go 语言在过去一年中的流行程度是稳中有升。golang


图片

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

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


图片

图 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(https://cn.hyperledger.org/)这个项目。再次是数据科学和机器学习这两个领域。不少人会说“这都是 Python 的底盘啊,并且几乎不可动摇”。可是我要说的是,其实在这两个领域中 Python 也不是一家独大的,像 R、Julia、MATLAB、C++ 等语言都有本身的一席之地。对标 numpy,最近持续升温的 Gonum(https://github.com/gonum/gonum)很不错。另外,Tensorflow 框架也早早放出了 Go 语言版本的 API。固然我不是鼓吹你们在这些领域中使用 Go 语言。各个语言在不一样领域都有各自的优点,重点是怎样提升生产力。可是我认为 Go 语言会在这些领域持续***并扩大优点。各位能够重点关注一下。

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

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

相关文章
相关标签/搜索