诞生
2007年9月20日的下午,在谷歌山景城总部的一间办公室里,谷歌的大佬级程序员Rob Pike启动了一个C++工程的编译构建。按照以往的经验判断,这次构建大约需要一个小时。利用这段时间,Rob Pike与谷歌的另两个大佬级程序员Robert Griesemer和Ken Thompson进行了一次有关设计一门新编程语言的讨论,而这次讨论成为Go语言诞生的“导火索”。
当时的谷歌内部主要使用C++语言构建各种系统,但C++复杂性高,编译构建速度慢,在编写服务端程序时不便支持并发。诸如此类的一些问题让三位大佬产生了设计一门新编程语言的想法。在他们的初步构想中,这门新语言应该是能够给程序员带来快乐、匹配未来硬件发展趋势并适合用来开发谷歌内部大规模程序的。
2007年9月25日,Rob Pike在一封回复电邮中把这门新编程语言命名为go。在Rob Pike的心目中,go这个单词短小、容易输入并且在组合其他字母后便可以用来命名Go相关的工具,比如编译器(goc)、汇编器(goa)、链接器(gol)等。
设计哲学
追求简单,少即是多
不同于那些通过相互借鉴而不断增加新特性的主流编程语言(如C++、Java等),Go的设计者们在语言设计之初就拒绝走语言特性融合的道路 ,而选择了“做减法”,选择了“简单”,他们把复杂性留给了语言自身的设计和实现,留给了Go核心开发组自己,而将简单、易用和清晰留给了广大Gopher。
- 简洁、常规的语法(不需要解析符号表),它仅有25个关键字;
- 内置垃圾收集,降低开发人员内存管理的心智负担;
- 显式依赖(package);
- 首字母大小写决定可见性;
- 任何类型都可以拥有方法(没有类);
- 内置字符串、切片(slice)、map类型;
- 内置数组边界检查;
- 内置并发支持; ......
偏好组合,正交解
C++、Java等主流面向对象(以下简称OO)语言通过庞大的自上而下的类型体系、继承、显式接口实现等机制将程序的各个部分耦合起来,但在Go语言中我们找不到经典OO的语法元素、类型体系和继承机制,或者说Go语言本质上就不属于经典OO语言范畴。
在语言设计层面,Go提供了正交的语法元素供后续组合使用,包括:
- Go语言无类型体系(type hierarchy),类型之间是独立的,没有子类型的概念;
- 每个类型都可以有自己的方法集合,类型定义与方法实现是正交独立的;
- 接口(interface)与其实现之间隐式关联;
- 包(package)之间是相对独立的,没有子包的概念
Go语言提供的最为直观的组合的语法元素是类型嵌入(type embedding)。通过类型嵌入,我们可以将已经实现的功能嵌入新类型中,以快速满足新类型的功能需求。这种方式有些类似经典OO语言中的继承机制,但在原理上与其完全不同,这是一种Go设计者们精心设计的语法糖。被嵌入的类型和新类型之间没有任何关系,甚至相互完全不知道对方的存在,更没有经典OO语言中的那种父类、子类的关系以及向上、向下转型(type casting)。在通过新类型实例调用方法时,方法的匹配取决于方法名字,而不是类型。
type poolLocal struct {
private interface{}
shared []interface{}
Mutex //在poolLocal这个结构体类型中嵌入了类型Mutex,被嵌入的Mutex类型的方法集合会被提升到外面的类型(poolLocal)中
pad [128]byte
}
通过在interface的定义中嵌入interface类型来实现接口行为的聚合,组成大接口,这种方式在标准库中尤为常用,并且已经成为Go语言的一种惯用法。
interface是Go语言中真正的“魔法”,是Go语言的一个创新设计,它只是方法集合,且与实现者之间的关系是隐式的,它让程序各个部分之间的耦合降至最低,同时是连接程序各个部分的“纽带”。隐式的interface实现会不经意间满足依赖抽象、里氏替换、接口隔离等设计原则。
// $GOROOT/src/io/io.go
type ReadWriter interface {
Reader
Writer
}
// $GOROOT/src/io/ioutil/ioutil.go
func ReadAll(r io.Reader)([]byte, error)
//函数ReadAll通过io.Reader这个接口将io.Reader的实现与ReadAll所在的包以低耦合的方式水平组合在一起了
原生并发,轻量高效
Go的设计者敏锐地把握了CPU向多核方向发展的这一趋势,在决定不再使用C++而去创建一门新语言的时候,果断将面向多核、原生内置并发支持
作为新语言的设计原则之一。
Go语言原生支持并发的设计哲学体现在以下几点。
(1)Go语言采用轻量级协程并发模型,使得Go应用在面向多核硬件时更具可扩展
操作系统调度器会将系统中的多个线程按照一定算法调度到物理CPU上运行。传统编程语言(如C、C++等)的并发实现实际上就是基于操作系统调度的,即程序负责创建线程(一般通过pthread等函数库调用实现),操作系统负责调度
go果断放弃了传统的基于操作系统线程的并发模型,而采用了用户层轻量级线程
或者说是类协程(coroutine)
,Go将之称为goroutine
。goroutine占用的资源非常少,Go运行时默认为每个goroutine分配的栈空间仅2KB。goroutine调度的切换也不用陷入(trap)操作系统内核层完成,代价很低。因此,在一个Go程序中可以创建成千上万个并发的goroutine。所有的Go代码都在goroutine中执行,哪怕是Go的运行时代码也不例外。
不过,一个Go程序对于操作系统来说只是一个用户层程序
。操作系统的眼中只有线程,它甚至不知道goroutine的存在。goroutine的调度全靠Go自己完成,实现Go程序内goroutine之间公平地竞争CPU资源的任务就落到了Go运行时头上。而将这些goroutine按照一定算法放到CPU上执行的程序就称为goroutine调度器(goroutine scheduler)。
。
(2)Go语言为开发者提供的支持并发的语法元素和机制
我们先来看看那些设计并诞生于单核年代的编程语言(如C、C++、Java)在语法元素和机制层面是如何支持并发的。
- 执行单元:线程。
- 创建和销毁的方式:调用库函数或调用对象方法。
- 并发线程间的通信:多基于操作系统提供的IPC机制,比如共享内存、Socket、Pipe等,当然也会使用有并发保护的全局变量。 与上述传统语言相比,Go提供了语言层面内置的并发语法元素和机制。
- 执行单元:goroutine。
- 创建和销毁方式:go+函数调用;函数退出即goroutine退出。
- 并发goroutine的通信:通过语言内置的channel传递消息或实现同步,并通过select实现多路channel的并发控制。
参考文献
《Go语言精进之路》