解读 2016 之 Golang 篇:极速提升,逐步超越

阅读数:12011 2016 年 12 月 15 日

Go 语言已经 7 岁了!今年 8 月,Go 1.7 如期发布。撰写本稿时,Go 1.8 的测试版也出来了。我们正在热切盼望着明年 2 月的 Go 1.8 正式版。

如果你关注 TIOBE 的编程语言排行榜就会发现,截止到 2016 年 11 月,Go 语言从原先的第 50 多位经过多次上窜已经跃到了第 13 位,跻入绝对主流的编程语言的行列!这份排行榜每月都会更新,并基于互联网上的程序员老鸟、教学课程和相关厂商的数量进行排名。在国内,从我这几年运营 Go 语言北京用户组的经历来看,可以明显地感觉到 Go 语言的在国内的大热。N 多初创互联网企业都选用 Go 语言作为他们的基础技术栈。我还发现,已经有在大数据、机器人等尖端科技领域耕耘的国内公司开始使用 Go 语言。这门语言现在已经是无孔不入了。

1. 回顾

遥想去年的 1.5 版本,Go 运行时系统和标准库刚完成去 C 化,转而完全由 Go 语言和汇编语言重写。到现在,Go 的源码已有了较大的改进,Go 语言版本的 Go 语言也更加成熟了。我下面就带领大家一起回顾一下 Go 语言在 2016 年做出的那些大动作。你可以对比我之前写的《解读2015 之Golang 篇:Golang 的全迸发时代》来看。

1.1 极速 GC

当然,首先要说的还是性能。Go 语言本身最大的性能提升依然在 GC(garbage collection,垃圾回收)方面。从 Go 1.5 时标榜的 GC 耗时百毫秒级,到今天的全并发 GC 使得耗时达到毫秒级,再到即将发布的 Go 1.8 由于实施了诸多改进而达成的百微秒级以下的 GC 耗时,真可谓是突飞猛进!

图 1 GC 停顿时间——Go 1.5 vs. Go 1.6

图 2 GC 停顿时间——Go 1.7

在经历了如此变化之后,如果你现在再说你的 Go 程序的性能瓶颈在 GC 上,那只能让人侧目了。

当然,Go 语言对自身性能的提升远不止于此。

1.2 对 HTTP/2 的支持

很早以前,Go 语言团队就开始跟进 HTTP/2 草案了。从 Go 1.6 开始,我们其实已经可以间接地在 Go 程序中使用到 HTTP/2 了,应用场景如:使用 Go 程序开发基于 HTTPS 协议的服务端和客户端。不过,这一切都是自动适配的,Go 官方并未暴露出可以指定或配置 HTTP/2 模块的任何 API。另外,在还未发布的 Go 1.8 中,HTTP/2 还会得到更广泛的支持。

1.3 httptrace 包

Go 1.7 的标准库中新增了 net/http/httptrace 代码包(https://godoc.org/net/http/httptrace)。它提供了一种调试 HTTP 请求和响应的方式。你可以像下面这样轻易地获取基于 HTTP 协议的通讯过程的详细信息。

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"net/http/httptrace"
	"os"
)

func main() {
	traceCtx := httptrace.WithClientTrace(context.Background(), &httptrace.ClientTrace{
		GetConn: func(hostPort string) {
			fmt.Printf("Prepare to get a connection for %s.\n", hostPort)
		},
		GotConn: func(info httptrace.GotConnInfo) {
			fmt.Printf("Got a connection: reused: %v, from the idle pool: %v.\n",
				info.Reused, info.WasIdle)
		},
		PutIdleConn: func(err error) {
			if err == nil {
				fmt.Println("Put a connection to the idle pool: ok.")
			} else {
				fmt.Println("Put a connection to the idle pool:", err.Error())
			}
		},
		ConnectStart: func(network, addr string) {
			fmt.Printf("Dialing... (%s:%s).\n", network, addr)
		},
		ConnectDone: func(network, addr string, err error) {
			if err == nil {
				fmt.Printf("Dial is done. (%s:%s)\n", network, addr)
			} else {
				fmt.Printf("Dial is done with error: %s. (%s:%s)\n", err, network, addr)
			}
		},
		WroteRequest: func(info httptrace.WroteRequestInfo) {
			if info.Err == nil {
				fmt.Println("Wrote a request: ok.")
			} else {
				fmt.Println("Wrote a request:", info.Err.Error())
			}
		},
		GotFirstResponseByte: func() {
			fmt.Println("Got the first response byte.")
		},
	})
	req, err := http.NewRequest("GET", "http://www.golang.org/", nil)
	if err != nil {
		log.Fatal("Fatal error:", err)
	}
	req = req.WithContext(traceCtx)
	_, err = http.DefaultClient.Do(req)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Request error: %v\n", err)
		os.Exit(1)
	}
}

强烈建议你动手运行一下这个小程序,享受一下掌控全局的感觉。

1.4 子测试

Go 1.7 中增加了对子测试(https://blog.golang.org/subtests)的支持,包括功能测试和性能测试。子测试的主要目的是在测试函数中区分和展示因不同的测试参数或测试数据带来的不同的测试结果。请看下面的测试程序。

package subtest

import (
	"fmt"
	"math/rand"
	"strconv"
	"testing"
)

// KE 代表键 - 元素对。
type KE struct {
	key     string
	element int
}

// BenchmarkMapPut 用于对字典的添加和修改操作进行测试。
func BenchmarkMapPut(b *testing.B) {
	max := 5
	var kes []KE
	for i := 0; i <= max; i++ {
		kes = append(kes, KE{strconv.Itoa(i), rand.Intn(1000000)})
	}
	m := make(map[string]int)
	b.ResetTimer()
	for _, ke := range kes {
		k, e := ke.key, ke.element
		b.Run(fmt.Sprintf("Key: %s, Element: %#v", k, e), func(b *testing.B) {
			for i := 0; i < b.N; i++ {
				m[k] = e + i
			}
		})
	}
}

在程序所在目录下使用 go test -run=^$ -bench . 命令运行它之后就会看到,针对每一个子测试,go test 命令都会打印出一行测试摘要。它们是分离的、独立统计的。这可以让我们进行更加精细的测试,细到每次输入输出。上述打印内容类似:

BenchmarkMapPut/Key:_0425,_Element:_498081-4         	30000000	        40.6 ns/op
BenchmarkMapPut/Key:_1540,_Element:_727887-4         	30000000	        41.7 ns/op
BenchmarkMapPut/Key:_2456,_Element:_131847-4         	30000000	        43.3 ns/op
BenchmarkMapPut/Key:_3300,_Element:_984059-4         	30000000	        46.1 ns/op
BenchmarkMapPut/Key:_4694,_Element:_902081-4         	30000000	        48.4 ns/op
BenchmarkMapPut/Key:_5511,_Element:_941318-4         	30000000	        59.3 ns/op
PASS
ok  	_/Users/haolin/infoq-2016_review_go /demo/subtest	8.678s

1.5 vendor 目录

在 Go 1.5 的时候,官方启用了一个新的环境变量——GO15VENDOREXPERIMENT。该环境变量可以启动 Go 的 vendor 目录(https://golang.org/s/go15vendor)并用于存放当前代码包依赖的代码包。在 Go 1.5 中,若 GO15VENDOREXPERIMENT 的值为 1 则会启动 vendor 目录。Go 1.6 正相反,默认支持 vendor 目录,当 GO15VENDOREXPERIMENT 的值为 0 时禁用 vendor 目录。到了 Go 1.7,官方完全去掉了这个环境变量。这也代表着对 vendor 目录的正式支持。Go 语言的实验特性一般都是按照类似的路数一步步迈向正式版的。

1.6 其他值得一提的改进

1.6.1 检测并报告对字典的非并发安全访问

从 Go 1.6 开始,Go 运行时系统对字典的非并发安全访问采取零容忍的态度。请看下面的程序。

package main

import "sync"

func main() {
	const workers = 100
	var wg sync.WaitGroup
	wg.Add(workers)
	m := map[int]int{}
	for i := 1; i <= workers; i++ {
		go func(i int) {
			for j := 0; j < i; j++ {
				m[i]++
			}
			wg.Done()
		}(i)
	}
	wg.Wait()
}

该程序在未施加任何保护的情况下在多个 Goroutine 中并发地访问了字典实例 m。我们知道,Go 原生的字典类型是非并发安全的。所以上面这样做很可能会让 m 的值产生不可预期的变化。这在并发程序中应该坚决避免。在 1.6 之前,如此操作的 Go 程序并不会因此崩溃。但是在 1.6,运行上述程序后就立刻会得到程序崩溃的结果。Go 运行时系统只要检测到类似代码,就会强制结束程序并报告错误。

1.6.2 sort 包的性能提升

Go 语言团队一直致力于标准库中众多 API 的性能提升,并且效果向来显著。我把 sort 包单拎出来强调是因为 sort.Sort 函数因性能优化而在行为上稍有调整。在 Go 1.6,sort.Sort 函数减少了大约 10% 的比较操作和交换操作的次数,从而获得了 20%~50% 的性能提升。不过,这里有一个副作用,那就是 sort.Sort 函数的执行会使排序算法不稳定。所谓不稳定的排序算法,就是排序可能会使排序因子相等的多个元素在顺序上不确定。比如,有如下需要根据长度排序的字符串的切片:

var langs= []string{"golang", "erlang", "java", "python", "php", "c++", "perl"}

经 sort.Sort 函数排序后,该切片只几个长度相等的元素 golang、erlang 和 python 的先后顺序可能就不是这样了,可能会变成 erlang、golang、python。虽然它依然会依据排序因子(这里是字符串长度)进行完全正确的排序,但是如此确实可能对一些程序造成影响。

如果你需要稳定的排序,可以使用 sort.Stable 函数取而代之。

1.6.3 context 包进入标准库

在 Go 1.7 发布时,标准库中已经出现了一个名为 context 的代码包。该代码包原先的导入路径为 golang.org/x/context,而后者现在已经不存在了。context 包被正式引入标准库,并且标准库中的很多 API 都因此而做了改变。context.Context 类型的值可以协调多个 Groutine 中的代码执行“取消”操作,并且可以存储键值对。最重要的是它是并发安全的。与它协作的 API 都可以由外部控制执行“取消”操作,比如:取消一个 HTTP 请求的执行。

1.6.4 go tool trace 的增强

go tool trace 自 Go 1.5 正式加入以来,成为了 Go 程序调试的又一利器。到了 Go 1.7,它已经得到了大幅增强。比如,执行时间的缩短、跟踪信息的丰富,等等。

1.6.5 unicode 包现基于 Unicode 9.0/h4>

Go 1.7 升级了 unicode 包,使它支持 Unicode 9.0 标准。在这之前,它支持的 Unicode 8.0 标准。

1.6.6 新的编译器后端——SSA

SSA作为新的编译器后端,可以让编译器生成压缩比和执行效率都更高的代码,并为今后的进一步优化提供了更有力的支持。在性能方面,它可以让程序减少 5% 至 35% 的 CPU 使用时间。

到这里,我向大家展示了 Go 语言在 2016 年的一些显著变化。由于篇幅原因,还有很多 Go 运行时系统和标准库的改动没能提及。尤其是性能方面的改进一直在持续,并潜移默化地为广大 Go 程序员提供着底层红利。

我强烈建议所有 Go 程序员紧跟 Go 语言团队的脚步,升级版本,享受红利。

2. 展望

2.1 新版本

关于展望,莫过于广大 Go 程序员翘首期盼的 Go 1.8 了。这里提一下几个如甘霖般的特性。

  1. Go 编写的 HTTP 服务器支持平滑地关闭。这一功能早已由很多第三方代码包实现,但是这次官方终于给出了答案。
  2. 支持 HTTP/2 的 Server Push。这个就不多说了,肯定会比 Hijack 更好用。
  3. 新增了 plugin 包,你可以把一些 Go 程序作为插件动态地加载进你的程序了。
  4. 更广泛的上下文支持,自从标准库中有了 context 包,它就在很多地方起作用了。很多基于标准库的接口功能都可以执行“取消”操作。在 Go 1.8 中,范围将进一步扩大,比如:database/sql 包和 testing 包都对上下文进行了支持。
  5. sort 包的功能改进,对于切片,我们不用再为了使用它的排序功能去编写某个接口的实现类型。
  6. go test 命令有了新的标记:-mutexprofile。该标记用于提供关于锁争用的概要文件。
  7. 当然,最值得期待的仍然是 Go 在性能上的提升,尤其是 GC 方面,又要有一次飞跃了!另外,defer 语句的执行会比之前快整整两倍!cgo 的调用开销也降低了将近一半!

2.2 技术社区

其实,除了对 Go 语言本身的展望,我们也应该憧憬 Go 社区(尤其是国内 Go 社区)的发展。中国现在已经差不多是 Go 程序员最多的国家了。

如果打开 Github 上 Go 语言的 Wiki(https://github.com/golang/go/wiki/GoUsers)你就会发现,那里已经有一个非常长的列表了。其中的公司达到了近 200 家,相比于 2015 年年底翻了将近三倍。而且我相信这只是一小部分,只是在 Github 上有自己的官方组织且对社区有贡献的一部分公司。不过,你可能还会发现,在 China 那一栏下的公司却只有一家。这是为什么呢?比较值得深思。我想这也许能从侧面反映出对国际技术社区(尤其是开源社区)有贡献的国内公司太少的问题。在 2016 年 12 月初举办的一个大型开源技术盛典的讲台上,某开源公司 CEO 提到了程序员对开源社区应该不只索取更要奉献,这样才能更好地宣传和推销自己。同时,组织机构也不应该成为大家合作的瓶颈。但是,我想国内的实际情况却恰恰相反。我们国内的计算机技术公司,甚至技术驱动的互联网公司,大都没有为开源社区做奉献的习惯,甚至从规章制度上就是明令禁止的。从这方面看,我觉得那个列表中的 China 栏的惨状也着实不冤。我热切盼望到了明年这个 China 栏能够变长很多。

不过,从 Github 以及国内一些代码托管仓库上的 Go 项目数量上看,国人编写的 Go 软件其实已经非常多了。近年来崛起的国内 Go 开源项目已有不少,特别是(按 Star 数排列)Gogs、Beego、TiDB、Codis、Pholcus、Hprose、Cyclone 等等。他们都已经在国际或国内有了一定的影响力。另外,国人或华人参与的国际 Go 开源项目更是众多,比如很多人熟知的容器技术领域翘楚 Docker、Kubernates、Etcd,等等。

当然,除了一些拔尖的人和拔尖的项目。大多数中国 Go 程序员和爱好者还是只在国内活跃的。国内的很多地方都自行发起了 Go 语言用户组,包括但不限于:北京、上海、深圳、杭州,大连、香港等。在各个地方举办的 Go 语言技术聚会也更加专业、更加频繁,同时规模更大。仅在北京,2016 年参加此类聚会或活动的人次就将近 400,Go 语言北京用户组的微信公众号(golang-beijing)的粉丝数也超过了 2000。据悉,在 2017 年,各地的 Go 语言用户组还会有更大的动作。

我个人认为,如今 Go 语言的国内推广已经基本完成了科普阶段,现在我们可以实行更加轻松的推波助澜、顺水推舟的推广策略了。由于 Go 语言的优秀以及不断的进化,现在自发地关注 Go 语言的人越来越多了,尤其是在高等学府和编程新手的人群中。

Go 语言很好学,配套工具完善,开发和运行效率高,应用领域众多,国内社区也很活跃,有各种各样的中文资料和教程,进阶并不难,其工程化理念也相当得民心。如果你不是一点时间都没有的话,我建议你学一学这门简约、高效的编程语言。在互联网时代,尤其是是移动互联网时代,它已经大有作为。即使对于炙手可热的大数据、微服务等新型领域和理念而言,它也是一个相当重要的技术栈。甚至在即将爆发的人工智能和机器人大潮,我相信,Go 语言也必会大放异彩!

作者介绍

郝林,Go 语言北京用户组的发起人,极客学院 Go 语言课程顾问,著有图灵原创图书《Go 并发编程实战》,同时也是在线免费教程《Go 命令教程》和《Go 语言第一课》的作者。目前在微赛时代任平台技术负责人。

参考文献

  1. Go 1.6 Release Notes: https://tip.golang.org/doc/go1.6
  2. Go 1.7 Release Notes: https://tip.golang.org/doc/go1.7
  3. Go 1.8 Release Notes(Beta): https://tip.golang.org/doc/go1.8
  4. The State of Go(2016): https://talks.golang.org/2016/state-of-go.slide

收藏

评论

微博

发表评论

注册/登录 InfoQ 发表评论