“ Go is not meant to innovate programming theory. It’s meant to innovate programming practice. ”
-- Samuel Tesla [1]
所有关于Go语言的quotes中,笔者最喜欢的是这一句。
Go是一门挺有意思的语言,它诞生于Google,2009年底推出后曾经经历了短暂的热议,而后又归于沉寂,直到今年又才重新升温。TIOBE Index[2]给出的Go语言热度如下所示:
相比Java、C/C++这些老牌语言,Go还只能算是编程语言中的“小弟”,直到今年11月才刚刚爬到TIOBE Index历史最高的第13位:
这可能是近年来崛起的一些基于Go语言的项目,例如Docker[3]、Kubernetes[4]的功劳。
Go语言设计者之一Rob Pike在2012年接受采访时曾经道出了一段往事:其实Go语言设计之初是为了替代C++的。结果令人尴尬的是,C++的程序员们反应冷淡(“don't seem to care”),来用Go的程序员们大多来自Python和Ruby。[5]
但笔者却正好是个反例。作为一名以C++为“母语”的程序员,当笔者接触到了Go语言后,确实有一种打开了新世界窗户的感觉,并认为“C++基本上可以扔了”。因为 在Go语言的设计中总是处处体现着Google在开发中的 (best) practice ,而这也正是笔者所梦寐以求的。(从这个意义上来说,Rob Pike们可以安心了,总有人懂你们!)
本文不做Go语言入门,也不做语言优劣之争,只谈笔者个人对Go语言的感受,以及对Go语言在编程实践方面的理解:
可能有不少刚接触Go语言的人会问这样一些问题:为什么要有GOPATH这样一个东西?编译的时候指定项目目录不就好了吗,为什么要弄一个目录放置所有Go代码,再设置一个全局变量告诉Go编译器这个目录在哪?为什么 GOPATH 下要分src/bin/pkg三个目录?其实这些都源自Google独特的单根代码库开发模式及其配套的编译构建体系(参见拙著《从Google的单代码库模式看Google工程文化》[6])。Go的代码管理就像Google的单根管理形式。Go编译器的基本命令,例如go build、go test等,长得就像Google的blaze编译系统(已开源为bazel[7])那样。另外,Go语言还有go get/install命令进行包管理。
这些让笔者感到非常熟悉和亲切。在Google进行C++开发是件非常轻松的事情:基础库做得非常完善(绝大多数都是自己写的),依赖管理也做得非常好,编译构建架构成熟且易用。但在Google外做C++开发会发现有许多困难,比如C++的依赖管理是让人很头疼的事情,而且经常碰到二进制版本不兼容的问题。又比如make file是一件很“反人类”的东西,远远没有blaze/bazel的BUILD file易用。Go语言借鉴了Google在这方面的经验,从语言工具的层面解决了这些问题。
这一点笔者想单独拎出来说说。C++自身没有什么单元测试框架,但单元测试对于质量保证实在太重要了。Go语言自带testing包,结合go test命令可以完美运行单元测试,整套东西基本上就是gtest[8] + blaze test的复刻版,用起来十分的简单。
统一的coding style有利于团队协作和代码的可维护性。Go语言的coding style一直以来是争议比较大的,因为有许多语法层面的规定,例如小写开头的任何变量(包括函数)都是package scope的(类似Java里面public/protected/private什么都不加那种),大写开头的任何变量都是public的(类似Java里的public)。又比如花括号的开头必须放在前一行的末尾,而不能放在下一行的开头独占一行(后者甚至不能编译!)。这些规定限制了代码风格的多样性,有利于统一风格。另外,Go语言提供了程序格式化工具go fmt,会自动对程序进行一定程度上的风格归一化(例如空格变tab,import按字典序排序等)。
不过不像其它主流语言在Google都有非常详尽的coding style[9],目前Go语言没有推出过官方“细致版”coding style,只有《Effective Go》[10]和[11]算是官方的一些指导性建议,所以Go语言的coding style只能算是“较为统一”的。(这里顺带吐槽一下Go语言的许多coding style建议 似乎是 与Google其它语言背道而驰的。例如Go语言用tab而不用space,偏向用短变量名而非更具描述性的长变量名。笔者至今没有想通为什么...)
微服务(Microservice)模式近年来随着分布式计算和Docker等技术的发展而火热起来。微服务之间的调用需要RPC。作为Google的亲儿子,Google在其开源的RPC框架gRPC[12]中理所当然的加入了对Go语言的支持。不过Go语言表示:我自己也天生带RPC框架的。Go语言的net/rpc包中自带了RPC框架,可以通过gob(Go语言的object编码格式)、json或用户自定义的编码格式进行RPC调用,开发起来非常的简便。不过,Go语言自己的RPC只针对Go语言写的服务有效,如果要跨语言还是要用gRPC,通过protocol buffers做数据交换。
另外,出于Google的习惯(C++亦是如此),Go语言默认采用static linking的方式生成binary。这意味着部署的时候无需担心服务器上是否有外部依赖的动态链接库,只需一个binary一切搞定。因此,即使没有Docker这样的容器框架保障部署环境,Go产出的binary依旧能更容易的独立工作。
这是concurrent programming中的一个原则,在Go语言标志性的 goroutine+channel模式 下体现得淋漓尽致。而且goroutine+channel+select的模式让concurrency变得难以置信的简单,可以轻松启动大量并行任务,无需像在其它语言里那样考虑线程/线程池的创建和维护;可以轻松实现服务中经常出现的timeout、cancel、fan out query等逻辑(如果有兴趣可以参考[13]、[14]、[15])。
监控也是服务稳定性相当重要的一环。White-box monitoring指直接监控系统内部指标的监控方式。在Google运行的所有service都会自带一个http的varz服务,其中可以暴露很多服务的关键指标,然后通过borgmon收集、统计,在出现异常时报警。 [16]
Go语言也自带了类似的功能。通过expvar包,你可以轻松的将自己认为重要的系统关键指标暴露出来(默认暴露到http://你的服务ip:port/debug/vars),既方便人肉查看,也可以通过Prometheus[17]等监控框架收集数据,做监控和报警。
Profiling是查找程序瓶颈,提高程序效率的重要手段。Google有C++版本的profiling tool[18],Go语言也继承了这个传统,通过runtime/pprof包和go tool pprof可以方便的对程序进行profiling。同时类似expvar包,Go语言还可通过net/http/pprof包实现用http服务暴露pprof数据( 默认暴露到http://你的服务ip:port/debug/pprof ),方便数据的查看和收集。
诞生于Google的Go语言天生就是Google编程实践的集成者,尽管也有一些令人头疼的地方(以后有机会再说,嘿嘿),它还是让笔者倍感亲切和惊喜!
[1] 摘自 Ivo Balbaert 《 The Way to Go: A Thorough Introduction to the Go Programming Language》
[2] http://www.tiobe.com/tiobe-index/
[3] https://www.docker.com/
[4] http://kubernetes.io/
[5] https://commandcenter.blogspot.de/2012/06/less-is-exponentially-more.html
[6] http://dwz.cn/4ItEZv
[7] https://bazel.build/
[8] https://github.com/google/googletest
[9] https://github.com/google/styleguide
[10] https://golang.org/doc/effective_go.html
[11] https://github.com/golang/go/wiki/CodeReviewComments
[12] http://www.grpc.io/
[13] https://blog.golang.org/go-concurrency-patterns-timing-out-and
[14] https://blog.golang.org/pipelines
[15] https://blog.golang.org/context
[16] Betsy Beyer, et al.《Site Reliability Engineering: How Google Runs Production Systems》
[17] https://prometheus.io/
[18] https://github.com/gperftools/gperftools