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
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了。这里提一下几个如甘霖般的特性。
- Go编写的HTTP服务器支持平滑地关闭。这一功能早已由很多第三方代码包实现,但是这次官方终于给出了答案。
- 支持HTTP/2的Server Push。这个就不多说了,肯定会比Hijack更好用。
- 新增了plugin包,你可以把一些Go程序作为插件动态地加载进你的程序了。
- 更广泛的上下文支持,自从标准库中有了context包,它就在很多地方起作用了。很多基于标准库的接口功能都可以执行“取消”操作。在Go 1.8中,范围将进一步扩大,比如:database/sql包和testing包都对上下文进行了支持。
- sort包的功能改进,对于切片,我们不用再为了使用它的排序功能去编写某个接口的实现类型。
- go test命令有了新的标记:-mutexprofile。该标记用于提供关于锁争用的概要文件。
- 当然,最值得期待的仍然是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语言第一课》的作者。目前在微赛时代任平台技术负责人。
参考文献
- Go 1.6 Release Notes: https://tip.golang.org/doc/go1.6
- Go 1.7 Release Notes: https://tip.golang.org/doc/go1.7
- Go 1.8 Release Notes(Beta): https://tip.golang.org/doc/go1.8
- The State of Go(2016): https://talks.golang.org/2016/state-of-go.slide