前言
Go 是一个开源的编程语言,它能让构造简单、可靠且高效的软件变得容易[1]。
Go 语言被设计成一门应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言。对于高性能分布式系统领域而言,Go语言无疑比大多数其它语言有着更高的开发效率。它提供了海量并行的支持,这对于游戏服务端的开发而言是再好不过了[1]。
其实早在2018年前,我就已经有在国内的程序员环境中断断续续地听到Go语言的消息,Go语言提供的方便的并发编程方式,十分适合我当时选择的毕业设计选题,但是受限于导师的语言选择、项目的进度追赶、考研的时间压榨,一直没有机会来好好地学习这门语言。
在进入研究生阶段后,尽管研究的方向和算法相关,但未来的职业方向还是选择了以后端为主,主要是因为想做更多和业务相关的工作。为了能在有限的时间里给予自己足够深的知识底蕴,选择了一些让自己去深入了解的方向,Go语言自然也在其中,今天终于有机会来开始研究这门语言。
为什么要用Go语言?
撰写此文的初衷,是本文的标题,也是我作为初学者一直以来的疑问:
“我为什么要用Go语言?”
为了回答这个问题,我翻阅了很多Go语言相关的文档、书籍和教程,我发现我很难在它们之中找到非常明显直接的答案,书上和教程只会说,“是的,Go语言好用”。
对于部分人来说,这个问题的答案或许很“明显”,比如选择Go语言是因为Google设计的语言、Go开发赚的钱多、XX公司使用Go语言等等,如果想要了解这门语言更加本质的东西,仅仅这些答案我认为是还不够的。
部分Go的教徒可能会说,他们选择的理由是和语言本身相关的,比如:
- Go编译快
- Go执行快
- Go并发编程方便
- Go有垃圾回收(Garbage Collection, GC)
的确,Go是有这些特点,但这并非都是Go独有的:
- 运行时解释的脚本语言(比如Python)几乎不需要时间编译
- C、C++甚至是汇编,基本上能够榨干一台机器的大部分性能
- 大部分语言都有并发编程的支持库
- 大部分语言都不需要程序员主动关注内存情况
一些Go的忠实粉丝把这种All in One的特性作为评价语言的标准,他们认为至少在这些方面,Go是可以完美的代替其他语言的。
那么,Go真的能优秀到完全替代另一个语言么?
其实未必,我始终认为银弹是不存在的[2],无论是在这次调查前,还是在这次调查后。
本文从Go语言被设计的初衷出发,深入互联网各种角落,调查Go所具有的那些特性是否足够优秀,同时和其他语言进行适当的比较,你可以选择性的阅读、接受或者反对我的内容,毕竟有交流才能传播知识。
我的最终目的是让更多的初学者看到Go没有轻易暴露出的缺点,同时也能看到Go真正优秀的地方。
设计Go的初衷
Go语言的主要目标是将静态语言的安全性和高效性与动态语言的易开发性进行有机结合,达到完美平衡,从而使编程变得更加有乐趣,而不是在艰难抉择中痛苦前行[3]。
Google公司不可能无缘无故地设计一个新语言(一些特性相比于其他语言也没有新到哪里去),这一切肯定是有原因的。
设计Go语言是为了解决当时Google开发遇到的一些问题[4]:
- C++编译慢、没有现代化(入门级友好的)的内存管理
- 数以万计行的代码,难以维护
- 部署的平台各式各样,交叉编译困难
- ......
找不到什么合适的语言,想着反正都是弄来自己用,Google选择造个轮子试试。
Go 语言起源 2007 年,并于 2009 年正式对外发布。它从 2009 年 9 月 21 日开始作为谷歌公司 20%兼职项目,即相关员工利用 20% 的空余时间来参与 Go 语言的研发工作。该项目的三位领导者均是著名的 IT 工程师:Robert Griesemer,参与开发 Java HotSpot 虚拟机;Rob Pike,Go 语言项目总负责人,贝尔实验室 Unix 团队成员,参与的项目包括 Plan 9,Inferno 操作系统和 Limbo 编程语言;Ken Thompson,贝尔实验室 Unix 团队成员,C 语言、Unix 和 Plan 9 的创始人之一,与 Rob Pike 共同开发了 UTF-8 字符集规范。自 2008 年 1 月起,Ken Thompson 就开始研发一款以 C 语言为目标结果的编译器来拓展 Go 语言的设计思想[3]。
当时Google的很多工程师是用的都是C/C++,所以语法的设计上接近于C,Go的设计师们想要解决其他语言使用中的缺点,但是仍保留他们的优点[5]:
- 静态类型和运行时效率
- 可读性和易用性
- 高性能的网络和多进程
- ...
emmm,这些听起来还是比较玄乎,毕竟设计归设计,实现归实现,我们回顾一下现在Go的几个主要特点,编译速度、执行速度、内存管理以及并发编程。
Go的编译为什么快
当然,设计Go语言也不是完全从零开始,最初Go的团队尝试设计实现一个Go语言的编译前端,由基于C的gcc编译器来编译成机器代码,这个面向gcc的前端编译器也就是目前的Go编译器之一的gccgo。
与其说Go的编译为什么快,不如先说说C++的编译为什么慢,C++也可以用gcc编译,编译速度的大部分差异很有可能来源于语言设计本身。
在讨论问题之前,其中需要先说明的一点是:这里比较的编译速度都是在静态编译下的。
静态编译和动态编译的区别:
- 静态编译:编译器在编译可执行文件时,要把使用到的链接库提取出来,链接打包进可执行文件中,编译结果只有一个可执行文件。
- 动态编译:可执行文件需要附带独立的库文件,不打包库到可执行文件中,减少可执行文件体积,在执行的时候再调用库即可。
两种方式有各自的优点和缺点,前者不需要去管理不同版本库的兼容性问题,后者可以减少内存和存储的占用(因为可以让不同程序共享同一个库),两种方式孰优孰弱,要对应到具体的工程问题上,Go默认的编译方式是静态编译。
回到我们要讨论的问题:C++的编译为什么慢?
C++编译慢的主要两个大头原因[6]:
- 头文件的include方式
- 模板的编译
C++使用include方式引用头文件,会让需要编译的代码有乘数级的增加,例如当同一个头文件被同一个项目下的N个文件include时,编译器会将头文件引入到每一份代码中,所以同一个头文件会被编译N次(这在大多数时候都是不必要的);C++使用的模板是为了支持泛型编程,在编写对不同类型的泛型函数时,可以提供很大的便利,但是这对于编译器来说,会增加非常多不必要的编译负担。
当然C++对这两个问题有很多后续的优化方法,但是这对于很多开发者来说,他们不想在这上面有过多时间和精力开销。
大部分后来的编程语言在引入文件的方式上,使用了import module来代替include 头文件的方式,import解决了重复编译的问题,当然Go也是使用的import方式;在模板的编译问题上,由于Go在设计理念上遵循从简入手,所以没有将泛函编程纳入到设计框架中,所以天生的没有模版编译带来的时间开销(没有泛型支持也是很多人不满Go语言的理由)。
在Go 的1.5 版本中,Go团队使用Go语言来编写Go语言的编译器(也叫自举),相比于gccgo来说:
- 提高了编译速度,但执行速度略有下降(性能细节优化还不如gcc)
- 增加了可编译的平台类型(以往受限于gcc)
在此之外,Go语言语法中的关键字也是非常少的(Go1.11版本里只有25个)[7],这也可以减少编译器花费在语法解析上的时间开销。
所以在我看来,Go编译速度快,主要出于四个原因:
- 使用了import的引用管理方式;
- 没有模板的编译负担;
- 1.5版本后的自举编译器优化;
- 更少的关键字。
所以为了加快编译速度、放弃C++而转入Go的同时,也要考虑一下是否要放弃泛型编程的优点。
注:泛型可能在Go 2版本获得支持。
Go的实际性能如何
Go的执行速度,可以参考一个语言性能测试数据网站 —— The Computer Language Benchmarks Game[8]。
这个网站在不同的算法上对每个语言进行测试,然后给出时间和内存上的开销数据比对。
比较的语言有C++、Java、Python。
首先是时间开销:
注意:时间开销的单位是s,并且Y轴为了方便进行不同跨度上的比较,所以选取的是对数轴(即非线性轴,为1-10-100-1000的比较跨度)。
然后是内存开销:
注意:Y轴为了方便进行不同跨度上的比较,所以选取的是对数轴(即非线性轴,为1000-10000-100000-1000000的比较跨度)。
需要注意的是,语言本身的性能只决定了一个程序的最高理论性能,程序具体的性能还要取决于这个程序的实现方法,所以当各个语言的性能并没有太大的差异时,性能往往只取决于程序实现的方式。
通过两个图的数据可以分析:
- Go虽然还无法达到C++那样的极致性能,但是在大部分情况下已经很接近了;
- Go和Java在算法的时间开销上难分伯仲,但在内存的开销上Java就要高得多了;
- Go在上述的绝大部分情况下,至少时间和内存开销都比Python要优秀得多;
Go的并发编程
Go的并发之所以比较受欢迎,网络上的很多内容集中在几个方面:
- 天生并发的设计
- 轻量化的并发编程方式
- 较高的并发性能
- 轻量级线程Goroutines、并发通信Channels以及其他便捷的并发同步控制工具
由于Go在设计的时候就考虑到了并发的支持,或者说很多特性都是为了并发而设计,这和一些后期库支持并发和第三方库支持并发的语言不同。
所以Go的并发到底有多方便?在Go中使用并发,只需要在普通的函数执行前加上一个go关键字,就可以新建一个线程让函数在其中执行:
- func main() {
- go loop() // 启动一个goroutine
- loop()
- }
这样带来的好处不仅仅是让并发编程更方便了,在一些特定情况下,比如Go引用一些使用了并发的库时,这些库所使用的并发也是基于Go本身的并发设计,不会存在库使用另一套并发实现的情况,这样Go调度器在处理程序中的各种并发线程时,可以有更加统一化的管理方式。
不过Go的并发对于程序的实现要求还是比较高的,在使用一些通信Channel的场合,稍有疏忽就可能出现死锁的问题,比如:
- fatal error: all goroutines are asleep - deadlock!
Go的并发量可以比大部分语言里普通的线程实现要高,这受益于轻量级的Goroutine,轻量化主要是它所占用的空间要小得多,例如64位环境下的JVM,它会默认固定为每个线程分配1MB的线程栈空间,而Goroutines大概只有4-8KB,之后再按需分配。足够轻量化的线程在相同的内存下也就可以有更高并发量(服务器CPU还没有饱和的情况下),同时也可以减少很多上下文切换的时间开销[9]。但是如果你的每个线程占用空间都非常大时(比如10MB,当然这是非常规需求的情况下),Go的轻量化优势就没有那么明显了。
Go在并发上的优点很明显,也是Go的功能目标,从语言设计上支持了并发,提供了统一便捷的工具,复杂的并发业务也需要在Go的一整套并发规范体系下进行编程,当然这肯定会牺牲部分实现自由度,但可以获得性能的提高和维护成本的下降。
PS:关于Go调度器的内容在这里并没有被提及,因为很难用简单的文字向读者说明该调度方式和其他调度方式的优劣,将在未来的某一篇中会细致地介绍Go调度器的内容。
Go的垃圾回收
垃圾回收(英语:Garbage Collection,缩写为GC),在计算机科学中是一种自动的存储器管理机制。当一个计算机上的动态存储器不再需要时,就应该予以释放,以让出存储器,这种存储器资源管理,称为垃圾回收。垃圾回收器可以让程序员减轻许多负担,也减少程序员犯错的机会[10]。
在使用Go或者其他支持GC的语言时,不用再像C++一样,手动地去释放不需要的变量占用的内容空间(free/delete)。
的确,这很方便(对于懒人和容易忘记主动释放的人),但是也多了一些限制(暗箱操作的不透明性以及在GC处理上的性能开销)。GC也不是万能的,当遇到一些对性能要求较高的场景,还是需要记得进行一些主动释放或优化操作(比如说自定义内存池)。
PS:将在未来的某一篇中会细致地介绍Go垃圾回收的细节(如果你们也觉得有必要的话)。
什么时候可以选择Go?
Go有很多优点,编译快、性能好、天生并发以及垃圾回收,很多比较有特色的内容也还没有说到(比如gofmt)。
Go语言也有很多缺点,比如第三方库支持还不够多(相比于Python来说就少的太多了)、支持编译的平台还不够广、还有被称为噩梦的依赖版本管理(已经在改善了,但是还没有达到完全可靠的程度)。
所以到底Go适合做什么,不适合做什么?
分析了这么多后,这个问题其实很难回答,但我们可以选择先从不适合的领域把Go剔除掉,看看我们会剩下什么。
Go不适合做什么
- 极致高性能优化的场景,你可能需要使用C/C++,甚至是汇编;
- 简单流程的脚本工具、数值分析、深度学习,可能Python更适合(至少目前是);
- 搭一个博客或网站,PHP何尝不是天下第一的语言呢;
- 如果你想比较方便找到一份的后端工作,绝大部分公司的Java岗一直缺人(在实际生产过程中,目前Go仍没有比Java表现得好太多,至少没有好到让一个部门/公司将核心业务重新转向Go来进行重构);
- ...
你可以找到类似上面那样的很多场景,你可能会发现Go并不能那么完美地替代掉谁。
Go适合做什么
最后,到了我们的终极问题,Go到底适合做什么?
读到这里你可能会觉得,好像是我把Go的特性吹了一遍,然后突然告诉你可能Go不适合你。
Go天生并发,面向并发,所以Go的定位一直很清楚,从最浅显的视角来看,至少Go作为一个有较高性能的并发后端来说,是具有非常大的诱惑力的。
尤其对于后端相关的程序员而言,在某些业务功能的初步实现上,简洁的语法、内置的并发、快速的编译,都可以让你更加高效快速地完成任务(前提是Go的内容足以完成你的任务),不用再去担忧编译优化和内存回收、不用担心过多的时间和内存开销、不用担心不同版本库之间的冲突(静态编译)以及不用担心交叉编译平台适配问题。
大部分情况下,编写一个服务,你只需要:实现、编译、部署、运行。
高效快速,足够敏捷,这在企业的绝大部分项目的初期都是适用的,这也是大部分项目对开发初期的要求。当一个项目或者服务真的可以发展下去,需求的确触碰到Go的天花板时,再考虑使用更加好的语言或方法去优化也为时不晚。
简而言之,尽管Go的过于简洁带来了很多问题(有些人说的难听点叫过于简单),Go所具有的优点,可以让大部分人用编程语言这种工具,来解决对他们而言更加重要的问题。
Go语言不是银弹,但它的确能有效地解决这些问题。