优势
第一个理由:对初学者足够友善,能够快速上手。
业界都公认:Go 是一种非常简单的语言。Go 的设计者们在发布 Go 1.0 版本和兼容性规范后,似乎就把主要精力放在精心打磨 Go 的实现、改进语言周边工具链,还有提升 Go 开发者体验上了。演化了十多年,Go 新增的语言特性也同样是"屈指可数"。
第二个理由:生产力与性能的最佳结合。
Go 创建的最初目的,就是构建流行的、高性能的服务器端编程语言,用以替代当时在这个领域使用较多的 Java 和 C++。而且,Go 也实现了它的这个目标。
Go 语言的性能在带有 GC 和运行时的语言中名列前茅,与不带 GC 的静态编程语言(比如 C/C++)之间也没有数量级的差距。在各大基准测试网站上,在相同的资源消耗水平的前提下,Go 的性能虽然低于 C++,但高出 Java 不少。
如果你熟悉的是动态语言,那也完全不用担心。Go 的大部分早期采用者,就来自动态语言程序员群体,包括 Python、JavaScript、Ruby 和 PHP 等语言的使用群体。因为和动态语言相比,Go 能够在保持生产力的同时,大幅度提高性能。虽然 Go 代码行数要多于 Python,但他们收获了近 10 倍的性能提升。
现在,Go 已经成为了云基础架构语言,它在云原生基础设施、中间件与云服务领域大放异彩。同时,GO 在 DevOps/SRE、区块链、命令行交互程序(CLI)、Web 服务,还有数据处理等方面也有大量拥趸,我们甚至可以看到 Go 在微控制器、机器人、游戏领域也有广泛应用。
第三个理由:快乐又有"钱景"。
Go 最初的目标,就是重新为开发人员带来快乐。这个快乐来自哪里呢?相比 C/C++,甚至是 Java 来说,Go 在开发体验上的确有很大提升。笼统来说,这个提升来自于简单的语法、得心应手的工具链、丰富和健壮的标准库,还有生产力与性能的完美结合、免除内存管理的心智负担,对并发设计的原生支持等等。而这些良好的体验,恰恰是你写 Go 代码时的快乐源泉。
有报告表明,在腾讯、字节跳动、Uber 等许多公司,Go 都是极其受欢迎,在字节跳动、Uber 内部甚至已经成长为主力语言。
诞生
Go 语言的创始人有三位,分别是图灵奖获得者、C 语法联合发明人、Unix 之父肯·汤普森(Ken Thompson),Plan 9 操作系统领导者、UTF-8 编码的最初设计者罗伯·派克(Rob Pike),以及 Java 的 HotSpot 虚拟机和 Chrome 浏览器的 JavaScript V8 引擎的设计者之一罗伯特·格瑞史莫(Robert Griesemer)。
之所以有这种想法,是因为当时的谷歌内部主要使用 C++ 语言构建各种系统,但 C++ 的巨大复杂性、编译构建速度慢以及在编写服务端程序时对并发支持的不足,让三位大佬觉得十分不便,他们就想着设计一门新的语言。在他们的初步构想中,这门新语言应该是能够给程序员带来快乐、匹配未来硬件发展趋势并适合用来开发谷歌内部大规模网络服务程序的。
特性
简单
在 Go 语言中看不到传统的面向对象的类、构造函数与继承,看不到结构化的异常处理,也看不到本属于函数编程范式的语法元素。
Go 语言自身实现起来并不容易,但这些复杂性被 Go 语言的设计者们"隐藏"了,所以 Go 语法层面上呈现了这样的状态:
- 仅有 25 个关键字,主流编程语言最少;
- 内置垃圾收集器,降低开发人员内存管理的心智负担;
- 首字母大小写决定可见性,无需通过额外关键字修饰;
- 变量初始为类型零值,避免以随机值作为初值的问题;
- 内置数组边界检查,极大减少越界访问带来的安全隐患;
- 内置并发支持,简化并发程序设计;
- 内置接口类型,为组合的设计哲学奠定基础;
- 原生提供完善的工具链,开箱即用;
- ... ...
显式
在 C 语言中,下面这段代码可以正常编译并输出正确结果:
c
//C 语言在编译c = a + b这一行时,会自动将短整型变量 a 和整型变量 b,先转换为 long 类型然后相加,并将所得结果存储在 long 类型变量 c 中。
#include <stdio.h>
int main() {
short int a = 5;
int b = 8;
long c = 0;
c = a + b;
printf("%ld\n", c);
}
那如果换成 Go 来实现这个计算会怎么样呢?
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 与 C 语言的隐式自动类型转换不同,Go 不允许不同类型的整型变量进行混合计算
因此,如果要使这段代码通过编译,我们就需要对变量 a 和 b 进行显式转型,就像下面代码段中这样:
go
c = int64(a) + int64(b)
fmt.Printf("%d\n", c)
除此之外,Go 语言采用了显式的基于值比较的错误处理方案,函数 / 方法中的错误都会通过 return 语句显式地返回,并且通常调用者不能忽略对返回的错误的处理。
这种有悖于"主流语言潮流"的错误处理机制还一度让开发者诟病,社区也提出了多个新错误处理方案,但或多或少都包含隐式的成分,都被 Go 开发团队一一否决了,这也与显式的设计哲学不无关系。
组合
Go 推崇的是组合的设计哲学。
在 Go 语言设计层面,Go 设计者为开发者们提供了正交的语法元素,以供后续组合使用,包括:
- Go 语言无类型层次体系,各类型之间是相互独立的,没有子类型的概念;
- 每个类型都可以有自己的方法集合,类型定义与方法实现是正交独立的;
- 实现某个接口时,无需像 Java 那样采用特定关键字修饰;
- 包之间是相对独立的,没有子包的概念。
Go 语言其实是为我们呈现了这样的一幅图景:一座座没有关联的"孤岛",但每个岛内又都很精彩。那么现在摆在面前的工作,就是在这些孤岛之间以最适当的方式建立关联,并形成一个整体。而 Go 选择采用的组合方式,也是最主要的方式。
Go 语言为支撑组合的设计提供了类型嵌入 (Type Embedding)。通过类型嵌入,我们可以将已经实现的功能嵌入到新类型中,以快速满足新类型的功能需求,这种方式有些类似经典面向对象语言中的"继承"机制。但区别其实很大
被嵌入的类型和新类型两者之间没有任何关系,甚至相互完全不知道对方的存在,更没有那种父类、子类的关系,以及向上、向下转型。
垂直组合
通过新类型实例调用方法时,方法的匹配主要取决于方法名字 ,而不是类型。这种组合方式,我称之为垂直组合,即通过类型嵌入,快速让一个新类型"复用"其他类型已经实现的能力,实现功能的垂直扩展。
go
// $GOROOT/src/sync/pool.go
type poolLocal struct {
private interface{}
shared []interface{}
Mutex
pad [128]byte
}
//我们在 poolLocal 这个结构体类型中嵌入了类型 Mutex,这就使得 poolLocal 这个类型具有了互斥同步的能力
// 我们可以通过 poolLocal 类型的变量,直接调用 Mutex 类型的方法 Lock 或 Unlock。
另外,我们在标准库中还会经常看到类似如下定义接口类型的代码段:
go
// $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 的数据源读取所有数据的目的。
并发
CPU 都是靠提高主频来改进性能的,但是主频提高导致 CPU 的功耗和发热量剧增,反过来制约了 CPU 性能的进一步提高。2007 年开始,处理器厂商的竞争焦点从主频转向了多核。
在这种大背景下,Go 的设计者在决定去创建一门新语言的时候,果断将面向多核、原生支持并发 作为了新语言的设计原则之一。并且,Go 放弃了传统的基于操作系统线程的并发模型,而采用了用户层轻量级线程 ,Go 将之称为 goroutine。
goroutine
占用的资源非常小,Go 运行时默认为每个 goroutine 分配的栈空间仅 2KB。goroutine 调度的切换也不用陷入(trap)操作系统内核层完成,代价很低。因此,一个 Go 程序中可以创建成千上万个并发的 goroutine。而且,所有的 Go 代码都在 goroutine 中执行,哪怕是 go 运行时的代码也不例外。- 同时,Go 还内置了并发设计的原语:
channel
和select
。开发者可以通过语言内置的 channel 传递消息或实现同步,并通过select
实现多路channel
的并发控制。
采用并发方案设计的程序在单核处理器上也是可以正常运行的,也许在单核上的处理性能可能不如非并发方案。但随着处理器核数的增多,并发方案可以自然地提高处理性能。
而且,并发与组合的哲学是一脉相承的,并发是一个更大的组合的概念,它在程序设计的全局层面对程序进行拆解组合,再映射到程序执行层面上:goroutines 各自执行特定的工作,通过 channel+select 将 goroutines 组合连接起来。
面向工程
Go 语言设计的初衷,就是面向解决真实世界中 Google 内部大规模软件开发存在的各种问题,为这些问题提供答案,这些问题包括:程序构建慢、依赖管理失控、代码难于理解、跨语言构建难等。
语法是编程语言的用户接口,它直接影响开发人员对于这门语言的使用体验。在面向工程设计哲学的驱使下,Go 在语法设计细节上做了精心的打磨。比如:
- 重新设计编译单元和目标文件格式,实现 Go 源码快速构建,让大工程的构建时间缩短到类似动态语言的交互式解释的编译速度;
- 如果源文件导入它不使用的包,则程序将无法编译。这可以充分保证任何 Go 程序的依赖树是精确的。这也可以保证在构建程序时不会编译额外的代码,从而最大限度地缩短编译时间;
- 去除包的循环依赖,循环依赖会在大规模的代码中引发问题,因为它们要求编译器同时处理更大的源文件集,这会减慢增量构建;
- 包路径是唯一的,而包名不必唯一的。导入路径必须唯一标识要导入的包,而名称只是包的使用者如何引用其内容的约定。
- 故意不支持默认函数参数。因为在规模工程中,很多开发者利用默认函数参数机制,向函数添加过多的参数以弥补函数 API 的设计缺陷,这会导致函数拥有太多的参数,降低清晰度和可读性;
- 增加类型别名(type alias),支持大规模代码库的重构。
由于诞生年代较晚,而且目标比较明确,Go 在标准库中提供了各类高质量且性能优良的功能包,其中的net/http、crypto、encoding
等包充分迎合了云原生时代的关于 API/RPC Web
服务的构建需求。
而且,开发人员在工程过程中肯定是需要使用工具的,Go 语言就提供了足以让所有其它主流语言开发人员羡慕的工具链,工具链涵盖了编译构建、代码格式化、包依赖管理、静态代码检查、测试、文档生成与查看、性能剖析、语言服务器、运行时程序跟踪等方方面面。
gofmt
这里值得重点介绍的是 gofmt
,它统一了 Go 语言的代码风格,Go 开发者可以更加专注于领域业务中。
在提供丰富的工具链的同时,Go 在标准库中提供了官方的词法分析器、语法解析器和类型检查器相关包,开发者可以基于这些包快速构建并扩展 Go 工具链。