go语言设计哲学

 设计哲学之于编程语言,就好比一个人的价值观之于这个人的行为。 如果你不知道为什么这么设计,会遇到很多很“别扭”的问题。

Go语言的设计哲学总结为五点:简单、显式、组合、并发和面向工程。

(1) 简单

Go 语言也没它看起来那么简单,自身实现起来并不容易,但这些复杂性被 Go 语言的设计者们“隐藏”了,所以 Go 语法层面上呈现了这样的状态:

仅有 25 个关键字,主流编程语言最少;
内置垃圾收集,降低开发人员内存管理的心智负担;
首字母大小写决定可见性,无需通过额外关键字修饰;
变量初始为类型零值,避免以随机值作为初值的问题;
内置数组边界检查,极大减少越界访问带来的安全隐患;
内置并发支持,简化并发程序设计;
内置接口类型,为组合的设计哲学奠定基础;
原生提供完善的工具链,开箱即用;

Go语言中看不到传统的面向对象的类、构造函数与继承,看不到结构化的异常处理,也看不到本属于函数编程范式的语法元素。

Go为什么不支持三目运算符

Go三目运算符杂谈
Go核心团队认为,程序员常常会利用三目运算符构建及其复杂的表达式,而这么复杂的表达式一定都可以通过拆解成一个或者多个 if 语句来实现,并且 if 语句的可读性更好。

Java里可能遇到NullPointerException的一个case

public static void main(String[] args) {
    Integer x = null;
    boolean b = (x == 0) ? true : false;  // Exception in thread "main" java.lang.NullPointerException
}

Java里遇到三目运算符套三目运算符时,很头疼

public static void main(String[] args) {
    Integer x = null;
    boolean b = (x == null) ? x == 0 ? true : false : x == 1 ? false : true;
    // boolean b = (x == null) ? (x == 0 ? true : false) : (x == 1 ? false : true);
}

go类型放在变量名的后面

类型放在变量名的后面是为了提高代码可读性的,golang的设计者在设计go语言的时候大概率参考了c语言的语法,针对存在大量变量需要声明的场景下,go的变量类型放在变量名的后面可读性会高很多,不容易引起类型定义的混乱。
比如:

int (*fp)(int a, int b);
int (*fp)(int (*ff)(int x, int y), int b)
f func(func(int,int) int, int) int
f func(func(int,int) int, int) func(int, int) int

go相比c,go的清晰易懂!而c的层层嵌套,难以直接看出是什么类型的指针

(2) 显式

Go 语言的第二大设计哲学:显式。

package main

import "fmt"

func main()  {
	var a byte = 1
	var b int = 2
	var c int
	c = a + b // Cannot use 'a + b' (type byte) as the type int    Invalid operation: a + b (mismatched types byte and int)
	fmt.Println(c)
}
command-line-arguments
./num-test.go:9:8: invalid operation: a + b (mismatched types byte and int)

如果我们编译这段程序,将得到类似这样的编译器错误:“invalid operation: a + b (mismatched types int16 and int)”。
Go 不允许不同类型的整型变量进行混合计算,它同样也不会对其进行隐式的自动转换。
 在Go语言中,不同类型变量是不能在一起进行混合计算的,这是因为 Go 希望开发人员明确知道自己在做什么

func main()  {
	var a byte = 1
	var b int = 2
	var c int
	c = a + b
	fmt.Println(c)
}

这么做的好处

避免一些错误,比如隐式类型转化导致的问题

Java里可能遇到的一个case

public static void main(String[] args) {
    char a = 'a'; // a转成int类型是97
    int i = 1;
    System.out.println(a + i); // 98
}

Java里可能遇到NullPointerException的一个case

public static void main(String[] args) {
    Integer x = null;
    boolean b = (x == 0) ? true : false;  // Exception in thread "main" java.lang.NullPointerException
}

不足

开发人员使用起来不方便
假如要使用字符串拼接,需要全转成string类型,然后才能拼接。

(3) 组合

Go 语言不像 C++、Java 等主流面向对象语言,我们在 Go 中是找不到经典的面向对象语法元素、类型体系和继承机制的,Go 推崇的是组合的设计哲学。

Go语言无类型层次体系,各类型之间是相互独立的,没有子类型的概念;
每个类型都可以有自己的方法集合,类型定义与方法实现是正交独立的;
实现某个接口时,无需像 Java 那样采用特定关键字修饰;
包之间是相对独立的,没有子包的概念。

Go 语言为支撑组合的设计提供了类型嵌入(Type Embedding)。通过类型嵌入,我们可以将已经实现的功能嵌入到新类型中,以快速满足新类型的功能需求,这种方式有些类似经典面向对象语言中的“继承”机制,但在原理上却与面向对象中的继承完全不同。

被嵌入的类型和新类型两者之间没有任何关系,甚至相互完全不知道对方的存在,更没有经典面向对象语言中的那种父类、子类的关系,以及向上、向下转型(Type Casting)。通过新类型实例调用方法时,方法的匹配主要取决于方法名字,而不是类型。这种组合方式,我称之为垂直组合,即通过类型嵌入,快速让一个新类型“复用”其他类型已经实现的能力,实现功能的垂直扩展。

垂直组合本质上是一种“能力继承”,采用嵌入方式定义的新类型继承了嵌入类型的能力。

Go 还有一种常见的组合方式,叫水平组合。和垂直组合的能力继承不同,水平组合是一种能力委托(Delegate),我们通常使用接口类型来实现水平组合。

(4) 并发

Go 的设计者在决定去创建一门新语言的时候,果断将面向多核、原生支持并发作为了新语言的设计原则之一。并且,Go 放弃了传统的基于操作系统线程的并发模型,而采用了用户层轻量级线程,Go 将之称为 goroutine。goroutine 占用的资源非常小,Go 运行时默认为每个 goroutine 分配的栈空间仅 2KB。goroutine 调度的切换也不用陷入(trap)操作系统内核层完成,代价很低。因此,一个 Go 程序中可以创建成千上万个并发的 goroutine。而且,所有的 Go 代码都在 goroutine 中执行,哪怕是 go 运行时的代码也不例外。

(5) 面向工程

Go 语言设计的初衷,就是面向解决真实世界中 Google 内部大规模软件开发存在的各种问题,为这些问题提供答案,这些问题包括:程序构建慢、依赖管理失控、代码难于理解、跨语言构建难等。

语法是编程语言的用户接口,它直接影响开发人员对于这门语言的使用体验。在面向工程设计哲学的驱使下,Go 在语法设计细节上做了精心的打磨。
比如:

重新设计编译单元和目标文件格式,实现 Go 源码快速构建,让大工程的构建时间缩短到类似动态语言的交互式解释的编译速度;
如果源文件导入它不使用的包,则程序将无法编译。这可以充分保证任何 Go 程序的依赖树是精确的。这也可以保证在构建程序时不会编译额外的代码,从而最大限度地缩短编译时间;
去除包的循环依赖,循环依赖会在大规模的代码中引发问题,因为它们要求编译器同时处理更大的源文件集,这会减慢增量构建;
包路径是唯一的,而包名不必唯一的。导入路径必须唯一标识要导入的包,而名称只是包的使用者如何引用其内容的约定。“包名称不必是唯一的”这个约定,大大降低了开发人员给包起唯一名字的心智负担;
故意不支持默认函数参数。因为在规模工程中,很多开发者利用默认函数参数机制,向函数添加过多的参数以弥补函数 API 的设计缺陷,这会导致函数拥有太多的参数,降低清晰度和可读性;
增加类型别名(type alias),支持大规模代码库的重构。

开发人员在工程过程中肯定是需要使用工具的,Go 语言就提供了足以让所有其它主流语言开发人员羡慕的工具链,工具链涵盖了编译构建、代码格式化、包依赖管理、静态代码检查、测试、文档生成与查看、性能剖析、语言服务器、运行时程序跟踪等方方面面。

参考资料

[1] 02|拒绝“Hello and Bye”:Go语言的设计哲学是怎么一回事?
[2] Go语言第一课FAQ