Go 语言的前生今世与介绍
一. Go 语言的发展
1.1 Go 语言是如何诞生的?
Go 语言的创始人有三位,分别是图灵奖获得者、C 语法联合发明人、Unix 之父肯·汤普森(Ken Thompson),Plan 9 操作系统领导者、UTF-8 编码的最初设计者罗伯·派克(Rob Pike),以及 Java 的 HotSpot 虚拟机和 Chrome 浏览器的 JavaScript V8 引擎的设计者之一罗伯特·格瑞史莫(Robert Griesemer)。
他们可能都没有想到,他们三个人在 2007 年 9 月 20 日下午的一次普通讨论,就这么成为了计算机编程语言领域的一次著名历史事件,开启了一个新编程语言的历史。
那天下午,在谷歌山景城总部的那间办公室里,罗伯·派克启动了一个 C++ 工程的编译构建。按照以往的经验判断,这次构建大约需要一个小时。利用这段时间,罗伯·派克和罗伯特·格瑞史莫、肯·汤普森坐在一处,交换了关于设计一门新编程语言的想法。
之所以有这种想法,是因为当时的谷歌内部主要使用 C++ 语言构建各种系统,但 C++ 的巨大复杂性、编译构建速度慢以及在编写服务端程序时对并发支持的不足,让三位大佬觉得十分不便,他们就想着设计一门新的语言。在他们的初步构想中,这门新语言应该是能够给程序员带来快乐、匹配未来硬件发展趋势并适合用来开发谷歌内部大规模网络服务程序的。
在第一天的简短讨论后,第二天这三位大佬又在谷歌总部的“雅温得(Yaounde)”会议室里具体讨论了这门新语言的设计。会后罗伯特·格瑞史莫发出了一封题为“prog lang discussion”的电邮,对这门新编程语言的功能特性做了初步的归纳总结:
这封电邮对这门新编程语言的功能特性做了归纳总结。主要思路是,在 C 语言的基础上,修正一些明显的缺陷,删除一些被诟病较多的特性,增加一些缺失的功能,比如:
-
使用import替代include。
-
去掉宏(macro)。
-
理想情况是用一个源文件替代.h和.c文件,模块的接口应该被自动提取出来(而无须手动在.h文件中声明)。
-
语句像C语言一样,但需要修正switch语句的缺陷。
-
表达式像C语言一样,但有一些注意事项(比如是否需要逗号表达式)。
-
基本上是强类型的,但可能需要支持运行时类型。
-
数组应该总是有边界检查。
-
具备垃圾回收的机制。
-
支持接口(interface)。
-
支持嵌套和匿名函数/闭包。
-
一个简单的编译器。
-
各种语言机制应该能产生可预测的代码。
这封电邮成为了这门新语言的第一版特性设计稿,三位大佬在这门语言的一些基础语法特性上达成了初步一致。
2007年 9 月 25 日,罗伯·派克在一封回复电邮中把这门新编程语言命名为“go”:
在罗伯·派克的心目中,“go”这个单词短小、容易输入并且在组合其他字母后便可以用来命名 Go 相关的工具,比如编译器(goc)、汇编器(goa)、链接器(gol)等(go 的早期版本曾如此命名 go 工具链,但后续版本撤销了这种命名方式,仅保留 go 这一统一的工具链名称 )。
这里有个误区,很多 Go 语言初学者经常称这门语言为 Golang,其实这是不对的:“Golang”仅应用于命名 Go 语言官方网站,而且当时没有用 go.com 纯粹是这个域名被迪士尼公司占用了而已。
1.2 Go语言的早期团队和演进历程
经过早期讨论,Go语言的三位作者在语言设计上达成初步一致,之后便开启了Go语言迭代设计和实现的过程。2008年年初,Unix之父Ken Thompson实现了第一版Go编译器,用于验证之前的设计。这个编译器先将Go代码转换为C代码,再由C编译器编译成二进制文件。到2008年年中,Go的第一版设计基本结束了。这时,同样在谷歌工作的Ian Lance Taylor为Go语言实现了一个GCC的前端,这也是Go语言的第二个编译器。Ian Lance Taylor的这一成果让三位作者十分喜悦,也很震惊。因为这对Go项目来说不仅仅是鼓励,更是一种对语言可行性的证明。Go语言的这第二个实现对确定语言规范和标准库是至关重要的。随后,Ian Lance Taylor以第四位成员的身份正式加入Go语言开发团队,并在后面的Go语言发展进程中成为Go语言及工具设计和实现的核心人物之一。Russ Cox也是在2008年加入刚成立不久的Go语言开发团队的,他是Go核心开发团队的第五位成员,他的一些天赋随即在Go语言设计和实现中展现出来。Russ Cox利用函数类型也可以拥有自己的方法这个特性巧妙设计出了http包的HandlerFunc类型,这样通过显式转型即可让一个普通函数成为满足http.Handler接口的类型。Russ Cox还在当时设计的基础上提出了一些更通用的想法,比如奠定了Go语言I/O结构模型的io.Reader和io.Writer接口。在Ken Thompson和Rob Pike先后淡出Go语言核心决策层后,Russ Cox正式接过两位大佬的衣钵,成为Go核心技术团队的负责人。到这里,Go 语言最初的核心团队形成,Go 语言迈上了稳定演化的道路。
1.3 Go语言正式发布并开源
2009 年 10 月 30 日,罗伯·派克在 Google Techtalk 上做了一次有关 Go 语言的演讲“The Go Programming Language”,这也是 Go 语言第一次公之于众。十天后,也就是 2009 年 11 月 10 日,谷歌官方宣布 Go 语言项目开源,之后这一天也被 Go 官方确定为 Go 语言的诞生日。
Go语言项目的主代码仓库位于go.googlesource.com/go。最初Go语言项目在code.google.com上建立了镜像仓库,几年后镜像仓库迁移到了GitHub上。
开源后的Go语言吸引了全世界开发者的目光。再加上Go的三位作者在业界的影响力以及谷歌的加持,越来越多有才华的程序员加入Go开发团队,越来越多贡献者开始为Go语言项目添砖加瓦。于是,Go在发布的当年(2009年)就成为著名编程语言排行榜TIOBE的年度最佳编程语言。
在Go开源后,一些技术公司,尤其是云计算领域的大厂以及初创公司,成为Go语言的早期接纳者。经过若干年的磨合,在这些公司中诞生了众多“杀手级”或示范性项目,如容器引擎Docker、云原生事实标准平台Kubernetes、服务网格Istio、区块链公链以太坊(Ethereum)、联盟链超级账本(Hyperledger Fabric)、分布式关系型数据库TiDB和CockroachDB、云原生监控系统Prometheus等。这些项目也让Go被誉为“云计算基础设施编程语言”。Go在近些年云原生领域的广泛应用也让其跻身云原生时代的头部编程语言。
在 Go 语言项目开源后,Go 语言也迎来了自己的“吉祥物”,是一只由罗伯·派克夫人芮妮·弗伦奇(Renee French)设计的地鼠,从此地鼠(gopher)也就成为了世界各地 Go 程序员的象征,Go 程序员也被昵称为 Gopher。
1.4 Go 语言的版本发展历史
2012 年 3 月 28 日,Go 1.0 版本正式发布,同时 Go 官方发布了“Go 1 兼容性”承诺:只要符合 Go 1 语言规范的源代码,Go 编译器将保证向后兼容(backwards compatible),也就是说我们使用新版编译器也可以正确编译用老版本语法编写的代码。
2009年11月10日,Go语言正式对外发布并开源。之后,Go语言在一段时间内采用了Weekly Release的模式,即每周发布一个版本。目前我们在Go语言的GitHub官方仓库中仍能找到早期的Weekly Release版本,比如weekly.2009-11-06。Go语言的版本发布历史如下。
从2011年3月7日开始,除了Weekly Release,Go项目还会每月发布一次,即Monthly Release,比如release.r56,这种情况一直延续到Go 1.0版本发布之前。2012年3月28日,Go 1.0正式发布。同时Go官方发布了“Go1兼容性”承诺:只要符合Go1语言规范的源代码,Go编译器将保证向后兼容(backwards compatible),即使用新版编译器可以正确编译使用老版本语法编写的代码。
2013年5月13日,Go 1.1版本发布,其主要的变动点如下。
新增method value语法:允许将方法绑定到一个特定的方法接收者(receiver)实例上,从而形成一个函数(function)。引入“终止语句”(terminating statement)的概念。将int类型在64位平台上的内部表示的字节数升为8字节,即64比特。更为精确的堆垃圾回收器(Heap GC),尤其是针对32位平台。
2013年12月1日,Go 1.2版本发布。从Go 1.2开始,Go开发组启动了以每6个月为一个发布周期的发布计划。Go 1.2版本的主要变动点包括:增加全切片表达式(Full slice expression):a[low: high: max];实现了在部分场景下支持抢占的goroutine调度器(利用在函数入口插入的调度器代码);go test新增-cover标志,用于计算测试覆盖率。
2014年6月18日,Go 1.3版本发布,其主要的变动点包括:支持更多平台,如Solaris、Dragonfly、Plan 9和NaCl等;goroutine的栈模型从分段栈(segmented stack)改为了连续栈(contiguous stack),改善Hot stack split问题;更为精确的栈垃圾回收器(Stack GC)。
2014年12月10日,Go 1.4版本发布。Go 1.4也是最后一个编译器和运行时由C语言实现的版本。其主要的变动点包括:新增for range x {…}形式的for-range语法;使用Go语言替换运行时的部分C语言实现,这使GC变得全面精确;由于连续栈的应用,goroutine的默认栈大小从8kB减小为2kB;增加internal package机制;增加canonical import path机制;新增go generate子命令,用于辅助实现代码生成;删除Go源码树中的src/pkg/xxx中的pkg这一级别,直接使用src/xxx。
**2015年8月19日,Go 1.5版本发布。Go 1.5是Go语言历史上的一个具有里程碑意义的重要版本。因为从这个版本开始,Go实现了自举,即无须再依赖C编译器。**然而Go编译器的性能比Go 1.4的C实现有了较大幅度的下降。其主要的变动点包括:Go编译器和运行时全部使用Go重写,原先的C代码实现被彻底移除;跨平台编译Go程序更为简洁,只需设置两个环境变量——GOARCH和GOOS即可;支持map类型字面量(literal);GOMAXPROCS的初始默认值由1改为运行环境的CPU核数;大幅度优化GC延迟,在多数情况下GC停止世界(Stop The World)的时间短于10ms;增加vendor机制,改善Go包依赖管理;增加go tool trace子命令;go build增加-buildmode命令选项,支持将Go代码编译为共享库(shared library)的形式。
2016年2月17日,Go 1.6版本发布,其主要的变动点包括:进一步优化GC延迟,实现Go程序在占用大内存的情况下,其GC延迟时间仍短于10ms;自动支持HTTP/2;定义了在C代码中共享Go指针的规则。
2016年8月15日,Go 1.7版本发布,其主要的变动点包括:针对x86-64实现了SSA后端,使得编译出的二进制文件大小减小20%~30%,而运行效率提升5%~35%;Go编译器的性能比Go 1.6版本提升近一倍;go test支持subtests和sub-benchmarks;标准库新增context包。
2017年2月16日,Go 1.8版本发布,其主要的变动点包括:支持在仅tags不同的两个struct之间进行显式类型转换;标准库增加sort.Slice函数;支持HTTP/2 Push机制;支持HTTP Server优雅退出;增加了对Mutex和RWMutex的profiling(剖析)支持;支持Go plugins,增加plugin包;支持默认的GOPATH路径($HOME/go),无须再显式设置;进一步优化SSA后端代码,程序平均性能提升10%左右;Go编译器性能进一步提升,平均编译链接的性能提升幅度在15%左右;GC的延迟进一步降低,GC停止世界(Stop The World)的时间通常不超过100μs,甚至不超过10μs;优化defer的实现,使得其性能损耗降低了一半。
2017年8月25日,Go 1.9版本发布,其主要的变动点包括:新增了type alias语法;在原来支持包级别的并发编译的基础上实现了包函数级别的并发编译,使得Go编译性能有10%左右的提升;大内存对象分配性能得到显著提升;增加了对单调时钟(monotonic clock)的支持;提供了一个支持并发的Map类型——sync.Map;增加math/bits包,将高性能位操作实现收入标准库。
2018年2月17日,Go 1.10版本发布,其主要的变动点包括:支持默认GOROOT,开发者无须显式设置GOROOT环境变量;增加GOTMPDIR环境变量;通过cache大幅提升构建和go test执行的性能,并基于源文件内容是否变化判定是否使用cache中的结果;支持Unicode 10.0版本。
2018年8月25日,Go 1.11版本发布。Go 1.11是Russ Cox在GopherCon 2017大会上发表题为“Toward Go 2”的演讲之后的第一个Go版本,它与Go 1.5版本一样也是具有里程碑意义的版本,因为它引入了新的Go包管理机制:Go module。Go module在Go 1.11中的落地为后续Go 2相关提议的渐进落地奠定了良好的基础。该版本主要的变动点包括:引入Go module,为Go包依赖管理提供了创新性的解决方案;引入对WebAssembly的支持,让Gopher可以使用Go语言来开发Web应用;为调试器增加了一个新的实验功能——允许在调试过程中动态调用Go函数。
2019年2月25日,Go 1.12版本发布,其主要的变动点包括:对Go 1.11版本中增加的Go module机制做了进一步优化;增加对TLS 1.3的支持;优化了存在大量堆内存(heap)时GC清理环节(sweep)的性能,使得一次GC后的内存分配延迟得以改善;运行时更加积极地将释放的内存归还给操作系统,以应对大块内存分配无法重用已存在堆空间的问题;该版本中Build cache默认开启并成为必需功能。
2019年9月4日,Go 1.13版本发布,其主要的变动点包括:增加以0b或0B开头的二进制数字字面量形式(如0b111)。增加以0o或0O开头的八进制数字字面量形式(如0o700)。增加以0x或0X开头的十六进制浮点数字面量形式(如0x123.86p+2)。支持在数字字面量中通过数字分隔符“_”提高可读性(如a := 5_3_7)。取消了移位操作(>>和<<)的右操作数只能是无符号数的限制。继续对Go module机制进行优化,包括:当GO111MODULE=auto时,无论是在
G
O
P
A
T
H
/
s
r
c
下还是
GOPATH/src下还是
GOPATH/src下还是GOPATH之外的仓库中,只要目录下有go.mod,Go编译器就会使用Go module来管理依赖;GOPROXY支持配置多个代理,默认值为https://proxy.golang.org,direct;提供了GOPRIVATE变量,用于指示哪些仓库下的module是私有的,即既不需要通过GOPROXY下载,也不需要通过GOSUMDB去验证其校验和。Go错误处理改善:在标准库中增加errors.Is和errors.As函数来解决错误值(error value)的比较判定问题,增加errors.Unwrap函数来解决error的展开(unwrap)问题。defer性能提升30%。支持的Unicode标准从10.0版本升级到Unicode 11.0版本。
2020年2月26日,Go 1.14版本发布,其主要的变动点包括:嵌入接口的方法集可重叠;基于系统信号机制实现了异步抢占式的goroutine调度;defer性能得以继续优化,理论上有30%的性能提升;Go module已经生产就绪,并支持subversion源码仓库;重新实现了运行时的timer;testing包的T和B类型都增加了自己的Cleanup方法。
2020年8月12日,Go 1.15版本发布,其主要的变动点包括:GOPROXY环境变量支持以管道符为分隔符的代理值列表;module的本地缓存路径可通过GOMODCACHE环境变量配置;运行时优化,将小整数([0, 255])转换为interface类型值时将不会额外分配内存;支持更为现代化的新版链接器,在Go 1.15版本中,新版链接器的性能相比老版本可提高20%,内存占用减少30%;增加tzdata包,可以用于操作附加到二进制文件中的时区信息。
2021年2月18日,Go 1.16版本发布,其主要的变动点包括:支持苹果的M1芯片(通过darwin/arm64组合);Go module-aware模式成为默认构建模式,即GO111MODULE值默认为on;go build/run命令不再自动更新go.mod和go.sum文件;go.mod中增加retract指示符以支持指示作废module的某个特定版本;引入GOVCS环境变量,控制获取module源码所使用的版本控制工具;GODEBUG环境变量支持跟踪包init函数的消耗;Go链接器得到进一步的现代化改造,相比于Go 1.15版本的链接器,新链接器的性能有20%25%的提升,资源占用下降5%15%,更为直观的是,编译出的二进制文件大小下降10%以上;新增io/fs包,建立Go原生文件系统抽象;新增embed包,作为在二进制文件中嵌入静态资源文件的官方方案。
2022年8月17日,Go 1.17版本发布,其主要的变动点包括:增加了slice对象直接强制类型转换为数组指针的能力;在unsafe包中新增Add和Slice函数。
2022年2月25日,Go 1.18版本发布,其主要的变动点包括:支持泛型;引入泛型后,容器(containers)包被拆分为sync、container/list等多个包;编译器和运行时改进,提升性能;crypto/tls支持TLS 1.3;net/http Client和Server实现HTTP/2和HTTP/3;新增net/http/httptrace标准化追踪HTTP请求。
2022年8月11日,Go 1.19版本发布,其主要的变动点包括:增强错误处理,通过error unwrapping等方式改善错误传递;crypto/rsa新增PSS padding模式;testing包新增测试步骤(T.Step);运行时优化,缩减程序大小;调度器优化,提升性能;支持在iOS和Android上交叉编译;各方面细节改进和bug修复。
2023年2月1日,Go 1.20版本发布,其主要的四个变化包括:支持直接切片转换成数组,增加转换的便利性;comparable约束放宽,允许更灵活的接口类型作为泛型参数;unsafe包新增语法糖函数,处理底层指针更便利;工具链优化升级,编译器引入PGO技术,安装包瘦身等。
2023年8月8日,Go 1.21版本发布,其主要的变动点包括:新增min、max、clear等内置函数,增强语言表达能力;改进了类型推断,增强了泛型支持;工具链增强了兼容性支持;标准库新增slices、maps、slog等包;保持与旧版本的高度兼容性。
总之,Go 语言发展得非常迅猛,从正式开源到现在,十二年的时间过去了,Go 语言发布了多个大版本更新,逐渐成熟。这里,我也梳理了迄今为止 Go 语言的重大版本更新,希望能帮助你快速了解 Go 语言的演化历史。
Go开发团队发布的Go语言稳定版本的平均质量一直是很高的,少有影响使用的重大bug。Go开发团队一直建议大家使用最新的发布版。
二. GO语言介绍
2.0 Go 语言原则
Go语言在设计之初就确定了三大原则:
- 简洁性(Simplicity): Go语言的语法简单直白,结构清晰。语法规则少,容易学习和使用。同时去掉了C++中未使用的复杂功能,就是让你用Python代码的开发效率编写C语言程序代码。
- 可读性(Readability): Go语言追求代码的简洁和可读性。通过格式化标准,命名规范等使代码易于阅读和理解。
- 功能性(Usability): Go语言注重软件工程中的可用性。内置并发、垃圾回收等功能让开发者可以高效编写软件。还提供了完善的标准库和工具。
2.1 为什么需要 Go 语言?
Go语言的设计目标之一是解决以下这些问题,提供更好的解决方案。以下是一些其他编程语言常见的弊端:
- 性能问题:一些高级编程语言可能在性能方面受到限制,因为它们的抽象层次较高,导致运行时性能不如低级语言。Go通过编译型语言的特性和并发性能优势来解决这个问题。
- 依赖管理:一些编程语言,尤其是C和C++,在管理依赖关系和外部库时可能面临复杂性。Go引入了Go module机制,用于更有效地管理依赖关系,解决了这个问题。
- 笨重:某些编程语言可能过于复杂,有大量的语法和特性,这可能使学习和使用变得困难。Go的设计目标之一是保持简洁和清晰,降低学习曲线。
- 垃圾回收和并行计算:对于一些系统编程语言,如C和C++,垃圾回收和并行计算等基础功能可能缺乏内置支持,需要开发者手动处理。Go通过内置垃圾回收和goroutine等机制来简化这些任务。
- 多核支持:在多核计算机上利用所有核心的能力是一项挑战。Go的goroutine和channel机制使并发编程变得更加容易,允许开发者有效地利用多核处理器。
2.2 Go 设计哲学
Go 语言的设计者们在语言设计之初,就拒绝了走语言特性融合的道路,选择了“做减法”并致力于打造一门简单的编程语言。
2.2.1 简单
Go 语法层面上呈现了这样的状态:
- 仅有 25 个关键字,主流编程语言最少;
- 内置垃圾收集,降低开发人员内存管理的心智负担;
- 首字母大小写决定可见性,无需通过额外关键字修饰;
- 变量初始为类型零值,避免以随机值作为初值的问题;
- 内置数组边界检查,极大减少越界访问带来的安全隐患;
- 内置并发支持,简化并发程序设计;
- 内置接口类型,为组合的设计哲学奠定基础;
- 原生提供完善的工具链,开箱即用;
Go 不会像 C++、Java 那样将其他编程语言的新特性兼蓄并收,所以你在 Go 语言中看不到传统的面向对象的类、构造函数与继承,看不到结构化的异常处理,也看不到本属于函数编程范式的语法元素。
2.2.2 显式
在 Go 语言中,不同类型变量是不能在一起进行混合计算的,这是因为 Go 希望开发人员明确知道自己在做什么,因此你需要以显式的方式通过转型统一参与计算各个变量的类型。
变量 a、b 和 c 的类型均不相同,使用Go编译程序会得到什么呢?
package main
import "fmt"
func main() {
var a int16 = 5
var b int = 8
var c int64
c = a + b
fmt.Printf("%d\n", c)
}
如果我们编译这段程序,将得到类似这样的编译器错误:“invalid operation: a + b (mismatched types int16 and int)”。
除此之外,Go 设计者所崇尚的显式哲学还直接决定了 Go 语言错误处理的形态:Go 语言采用了显式的基于值比较的错误处理方案,函数 / 方法中的错误都会通过 return 语句显式地返回,并且通常调用者不能忽略对返回的错误的处理。
这种有悖于“主流语言潮流”的错误处理机制还一度让开发者诟病,社区也提出了多个新错误处理方案,但或多或少都包含隐式的成分,都被 Go 开发团队一一否决了,这也与显式的设计哲学不无关系。
2.2.3 组合
在 Go 语言设计层面,Go 设计者为开发者们提供了正交的语法元素,以供后续组合使用,包括:
- Go 语言无类型层次体系,各类型之间是相互独立的,没有子类型的概念;
- 每个类型都可以有自己的方法集合,类型定义与方法实现是正交独立的;
- 实现某个接口时,无需像 Java 那样采用特定关键字修饰;
- 包之间是相对独立的,没有子包的概念。
无论是包、接口还是一个个具体的类型定义,Go 语言其实是为我们呈现了这样的一幅图景:一座座没有关联的“孤岛”,但每个岛内又都很精彩。那么现在摆在面前的工作,就是在这些孤岛之间以最适当的方式建立关联,并形成一个整体。而 Go 选择采用的组合方式,也是最主要的方式。
Go 语言为支撑组合的设计提供了类型嵌入(Type Embedding)。通过类型嵌入,我们可以将已经实现的功能嵌入到新类型中,以快速满足新类型的功能需求,这种方式有些类似经典面向对象语言中的“继承”机制,但在原理上却与面向对象中的继承完全不同,这是一种 Go 设计者们精心设计的“语法糖”。
被嵌入的类型和新类型两者之间没有任何关系,甚至相互完全不知道对方的存在,更没有经典面向对象语言中的那种父类、子类的关系,以及向上、向下转型(Type Casting)。通过新类型实例调用方法时,方法的匹配主要取决于方法名字,而不是类型。这种组合方式,我称之为垂直组合,即通过类型嵌入,快速让一个新类型“复用”其他类型已经实现的能力,实现功能的垂直扩展。
比如,下面这个 Go 标准库中的一段使用类型嵌入的组合方式的代码段:
// $GOROOT/src/sync/pool.go
type poolLocal struct {
private interface{}
shared []interface{}
Mutex
pad [128]byte
}
在代码段中,我们在 poolLocal
这个结构体类型中嵌入了类型 Mutex
,这就使得 poolLocal
这个类型具有了互斥同步的能力,我们可以通过 poolLocal
类型的变量,直接调用Mutex
类型的方法 Lock
或 Unlock
。
另外,我们在标准库中还会经常看到类似如下定义接口类型的代码段:
// $GOROOT/src/io/io.go
type ReadWriter interface {
Reader
Writer
}
这里,标准库通过嵌入接口类型的方式来实现接口行为的聚合,组成大接口,这种方式在标准库中尤为常用,并且已经成为了 Go 语言的一种惯用法。
垂直组合本质上是一种“能力继承”,采用嵌入方式定义的新类型继承了嵌入类型的能力。Go 还有一种常见的组合方式,叫水平组合。和垂直组合的能力继承不同,水平组合是一种能力委托(Delegate),我们通常使用接口类型来实现水平组合。
Go 语言中的接口是一个创新设计,它只是方法集合,并且它与实现者之间的关系无需通过显式关键字修饰,它让程序内部各部分之间的耦合降至最低,同时它也是连接程序各个部分之间“纽带”。
Go 语言中的接口是一个创新设计,它只是方法集合,并且它与实现者之间的关系无需通过显式关键字修饰,它让程序内部各部分之间的耦合降至最低,同时它也是连接程序各个部分之间“纽带”。
水平组合的模式有很多,比如一种常见方法就是,通过接受接口类型参数的普通函数进行组合,如以下代码段所示
// $GOROOT/src/io/ioutil/ioutil.go
func ReadAll(r io.Reader)([]byte, error)
// $GOROOT/src/io/io.go
func Copy(dst Writer, src Reader)(written int64, err error)
也就是说,函数 ReadAll 通过 io.Reader 这个接口,将 io.Reader 的实现与 ReadAll 所在的包低耦合地水平组合在一起了,从而达到从任意实现 io.Reader 的数据源读取所有数据的目的。类似的水平组合“模式”还有点缀器、中间件等,这里我就不展开了,在后面讲到接口类型时再详细叙述。
此外,我们还可以将 Go 语言内置的并发能力进行灵活组合以实现,比如,通过 goroutine+channel 的组合,可以实现类似 Unix Pipe 的能力。
总之,组合原则的应用实质上是塑造了 Go 程序的骨架结构。类型嵌入为类型提供了垂直扩展能力,而接口是水平组合的关键,它好比程序肌体上的“关节”,给予连接“关节”的两个部分各自“自由活动”的能力,而整体上又实现了某种功能。并且,组合也让遵循“简单”原则的 Go 语言,在表现力上丝毫不逊色于其他复杂的主流编程语言。
2.2.4 并发
“并发”这个设计哲学的出现有它的背景,你也知道 CPU 都是靠提高主频来改进性能的,但是现在这个做法已经遇到了瓶颈。主频提高导致 CPU 的功耗和发热量剧增,反过来制约了 CPU 性能的进一步提高。2007 年开始,处理器厂商的竞争焦点从主频转向了多核。
在这种大背景下,Go 的设计者在决定去创建一门新语言的时候,果断将面向多核、原生支持并发作为了新语言的设计原则之一。并且,Go 放弃了传统的基于操作系统线程的并发模型,而采用了用户层轻量级线程,Go 将之称为 goroutine。
goroutine 占用的资源非常小,Go 运行时默认为每个 goroutine 分配的栈空间仅 2KB。goroutine 调度的切换也不用陷入(trap)操作系统内核层完成,代价很低。因此,一个 Go 程序中可以创建成千上万个并发的 goroutine。而且,所有的 Go 代码都在 goroutine 中执行,哪怕是 go 运行时的代码也不例外。
在提供了开销较低的 goroutine 的同时,Go 还在语言层面内置了辅助并发设计的原语:channel 和 select。开发者可以通过语言内置的 channel 传递消息或实现同步,并通过 select 实现多路 channel 的并发控制。相较于传统复杂的线程并发模型,Go 对并发的原生支持将大大降低开发人员在开发并发程序时的心智负担。
此外,并发的设计哲学不仅仅让 Go 在语法层面提供了并发原语支持,其对 Go 应用程序设计的影响更为重要。并发是一种程序结构设计的方法,它使得并行成为可能。
采用并发方案设计的程序在单核处理器上也是可以正常运行的,也许在单核上的处理性能可能不如非并发方案。但随着处理器核数的增多,并发方案可以自然地提高处理性能。
而且,并发与组合的哲学是一脉相承的,并发是一个更大的组合的概念,它在程序设计的全局层面对程序进行拆解组合,再映射到程序执行层面上:goroutines 各自执行特定的工作,通过 channel+select 将 goroutines 组合连接起来。并发的存在鼓励程序员在程序设计时进行独立计算的分解,而对并发的原生支持让 Go 语言也更适应现代计算环境。
2.2.5 面向工程
Go 语言设计的初衷,就是面向解决真实世界中 Google 内部大规模软件开发存在的各种问题,为这些问题提供答案,这些问题包括:程序构建慢、依赖管理失控、代码难于理解、跨语言构建难等。
很多编程语言设计者和他们的粉丝们认为这些问题并不是一门编程语言应该去解决的,但 Go 语言的设计者并不这么看,他们在 Go 语言最初设计阶段就将解决工程问题作为 Go 的设计原则之一去考虑 Go 语法、工具链与标准库的设计,这也是 Go 与其他偏学院派、偏研究型的编程语言在设计思路上的一个重大差异。
语法是编程语言的用户接口,它直接影响开发人员对于这门语言的使用体验。在面向工程设计哲学的驱使下,Go 在语法设计细节上做了精心的打磨。比如:
- 重新设计编译单元和目标文件格式,实现 Go 源码快速构建,让大工程的构建时间缩短到类似动态语言的交互式解释的编译速度;
- 如果源文件导入它不使用的包,则程序将无法编译。这可以充分保证任何 Go 程序的依赖树是精确的。这也可以保证在构建程序时不会编译额外的代码,从而最大限度地缩短编译时间;
- 去除包的循环依赖,循环依赖会在大规模的代码中引发问题,因为它们要求编译器同时处理更大的源文件集,这会减慢增量构建;
- 包路径是唯一的,而包名不必唯一的。导入路径必须唯一标识要导入的包,而名称只是包的使用者如何引用其内容的约定。“包名称不必是唯一的”这个约定,大大降低了开发人员给包起唯一名字的心智负担;
- 故意不支持默认函数参数。因为在规模工程中,很多开发者利用默认函数参数机制,向函数添加过多的参数以弥补函数 API 的设计缺陷,这会导致函数拥有太多的参数,降低清晰度和可读性;
- 增加类型别名(type alias),支持大规模代码库的重构。
在标准库方面,Go 被称为“自带电池”的编程语言。如果说一门编程语言是“自带电池”,则说明这门语言标准库功能丰富,多数功能不需要依赖外部的第三方包或库,Go 语言恰恰就是这类编程语言。
由于诞生年代较晚,而且目标比较明确,Go 在标准库中提供了各类高质量且性能优良的功能包,其中的 net/http、crypto、encoding 等包充分迎合了云原生时代的关于 API/RPC Web 服务的构建需求,Go 开发者可以直接基于标准库提供的这些包实现一个满足生产要求的 API 服务,从而减少对外部第三方包或库的依赖,降低工程代码依赖管理的复杂性,也降低了开发人员学习第三方库的心理负担。
而且,开发人员在工程过程中肯定是需要使用工具的,Go 语言就提供了足以让所有其它主流语言开发人员羡慕的工具链,工具链涵盖了编译构建、代码格式化、包依赖管理、静态代码检查、测试、文档生成与查看、性能剖析、语言服务器、运行时程序跟踪等方方面面。
这里值得重点介绍的是 gofmt ,它统一了 Go 语言的代码风格,在其他语言开发者还在为代码风格争论不休的时候,Go 开发者可以更加专注于领域业务中。同时,相同的代码风格让以往困扰开发者的代码阅读、理解和评审工作变得容易了很多,至少 Go 开发者再也不会有那种因代码风格的不同而产生的陌生感。Go 的这种统一代码风格思路也在开始影响着后续新编程语言的设计,并且一些现有的主流编程语言也在借鉴 Go 的一些设计。
在提供丰富的工具链的同时,Go 在标准库中提供了官方的词法分析器、语法解析器和类型检查器相关包,开发者可以基于这些包快速构建并扩展 Go 工具链。
三. Go 语言的特性
3.1 编译型语言和解释型语言
编译型语言:
- 编译过程:在编译型语言中,源代码在运行之前必须通过编译器转换为机器码或虚拟机字节码。编译过程将整个程序翻译成一个可执行文件,该文件可以在目标计算机上独立运行。
- 执行速度:由于编译型语言的代码在运行前已经经过编译,因此它们通常具有很高的执行速度,因为机器可以直接执行编译后的代码,而无需解释。
- 开发迭代:在编译型语言中,如果需要对程序进行更改,通常需要重新编译整个程序,这可能会导致开发迭代速度较慢。
- 示例语言:C、C++、Go、Rust等都是编译型语言的示例。
解释型语言:
- 解释过程:在解释型语言中,源代码由解释器逐行解释执行,而不是先编译成机器码。解释器读取源代码的一行,执行它,然后再读取下一行。
- 执行速度:解释型语言通常比编译型语言执行速度较慢,因为代码需要在运行时逐行解释,而不是直接执行编译后的机器码。
- 开发迭代:解释型语言通常具有更快的开发迭代速度,因为开发者可以更轻松地修改和测试代码,无需重新编译整个程序。
- 示例语言:Python、JavaScript、Ruby、PHP等都是解释型语言的示例。
3.2 优点
Go 语言是一个可以编译高效,支持高并发的,面向垃圾回收的跨平台编译型全新语言。
- 秒级完成大型程序的单节点编译。
- 依赖管理清晰。
- 不支持继承,程序员无需花费精力定义不同类型之间的关系。
- 支持垃圾回收,支持并发执行,支持多线程通讯。
- 对多核计算机支持友好。
3.3 缺点:Go 语言不支持的特性
- 不支持函数重载和操作符重载
- 为了避免在 C/C++ 开发中的一些 Bug 和混乱,不支持隐式转换
- 支持接口抽象,支持面向对象和面向过程的编程模式,不支持继承
- 不支持动态加载代码
- 不支持动态链接库
- 通过 recover 和 panic 来替代异常机制
- 不支持断言
- 不支持静态变量
静态强类型语言:Go是一门静态类型语言,这意味着变量的类型在编译时已经确定,开发者需要在声明变量时指定其类型,且不允许在运行时将不同类型的值直接进行运算。这提供了类型安全性和代码可维护性,可以捕获潜在的类型错误。
编译型语言:Go是一门编译型语言,它的源代码需要通过编译器转换为机器码或虚拟机字节码,然后才能在目标平台上执行。这使得Go程序在运行时能够获得较高的性能,因为它不需要在每次运行时解释源代码。
3.4 Go语言为并发而生
硬件制造商正在为处理器添加越来越多的内核以提高性能。所有数据中心都在这些处理器上运行,更重要的是,今天的应用程序使用多个微服务来维护数据库连接,消息队列和维护缓存。因此,开发的软件和编程语言应该可以轻松地支持并发性,并且应该能够随着CPU核心数量的增加而可扩展。
但是,大多数现代编程语言(如Java,Python等)都来自90年代的单线程环境。虽然一些编程语言的框架在不断地提高多核资源使用效率,例如 Java 的 Netty 等,但仍然需要开发人员花费大量的时间和精力搞懂这些框架的运行原理后才能熟练掌握。
Go于2009年发布,当时多核处理器已经上市。Go语言在多核并发上拥有原生的设计优势,Go语言从底层原生支持并发,无须第三方库、开发者的编程技巧和开发经验。
很多公司,特别是中国的互联网公司,即将或者已经完成了使用 Go 语言改造旧系统的过程。经过 Go 语言重构的系统能使用更少的硬件资源获得更高的并发和I/O吞吐表现。充分挖掘硬件设备的潜力也满足当前精细化运营的市场大环境。
Go语言的并发是基于 goroutine
的,goroutine
类似于线程,但并非线程。可以将 goroutine
理解为一种虚拟线程。Go 语言运行时会参与调度 goroutine
,并将 goroutine
合理地分配到每个 CPU 中,最大限度地使用CPU性能。开启一个goroutine
的消耗非常小(大约2KB的内存),你可以轻松创建数百万个goroutine
。
goroutine
的特点:
goroutine
具有可增长的分段堆栈。这意味着它们只在需要时才会使用更多内存。goroutine
的启动时间比线程快。goroutine
原生支持利用channel安全地进行通信。goroutine
共享数据结构时无需使用互斥锁。
3.5 Go性能强悍
与其他现代高级语言(如Java/Python)相比,使用C,C++的最大好处是它们的性能。因为C/ C++是编译型语言而不是解释的语言。 处理器只能理解二进制文件,Java和Python这种高级语言在运行的时候需要先将人类可读的代码翻译成字节码,然后由专门的解释器再转变成处理器可以理解的二进制文件。
同C,C++一样,Go语言也是编译型的语言,它直接将人类可读的代码编译成了处理器可以直接运行的二进制文件,执行效率更高,性能更好。
数据来源:https://benchmarksgame-team.pages.debian.net/benchmarksgame/
可以看出,Go 语言在性能上更接近于 Java 语言,虽然在某些测试用例上不如经过多年优化的 Java 语言,但毕竟 Java 语言已经经历了多年的积累和优化。Go 语言在未来的版本中会通过不断的版本优化提高单核运行性能。
3.6 Go 语言特性衍生来源
3.7 Go语言命名:
命名规则
Go的函数、变量、常量、自定义类型、包(package)
的命名方式遵循以下规则:
- 首字符可以是任意的Unicode字符或者下划线
- 剩余字符可以是Unicode字符、下划线、数字
- 字符长度不限
Go 只有25个关键字
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
Go 还有37个保留字
Constants: true false iota nil
Types: int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64 complex128 complex64
bool byte rune string error
Functions: make len cap new append copy close delete
complex real imag
panic recover
可见性:
- 声明在函数内部,是函数的本地值,类似private
- 声明在函数外部,是对当前包可见(包内所有.go文件都可见)的全局值,类似protect
- 声明在函数外部且首字母大写是所有包可见的全局值,类似public
四. Go语言项目
docker:基于lxc的一个虚拟打包工具,能够实现PAAS平台的组建。
kubernetes :Kubernetes是Google开源的一个容器编排引擎,它支持自动化部署、大规模可伸缩、应用容器化管理。在生产环境中部署一个应用程序时,通常要部署该应用的多个实例以便对应用请求进行负载均衡
区块链:BTCD 是用go语言实现的完整节点的比特币实现
nsq:bitly开源的消息队列系统,性能非常高,目前他们每天处理数十亿条的消息
packer:用来生成不同平台的镜像文件,例如VM、vbox、AWS等,作者是vagrant的作者
skynet:分布式调度框架
Doozer:分布式同步工具,类似ZooKeeper
Heka:mazila开源的日志处理系统
cbfs:couchbase开源的分布式文件系统
tsuru:开源的PAAS平台,和SAE实现的功能一模一样
groupcache:memcahe作者写的用于Google下载系统的缓存系统
god:类似redis的缓存系统,但是支持分布式和扩展性
gor:网络流量抓包和重放工具