目录

Go的设计哲学

1. 我学过的编程语言

Visual Basic

我的编程语言学习过程,首先接触的Visual Basic,仅限于当时考完二级,而且是一次性通过(当时我们班第一次考过的,就只有两个),天赋我是没有的,有的只是没日没夜的练习吧,想起来那段日子,每天敲代码,做题目,还是很充实的。这门语言的排名在编程榜前十,但大学那会只是考试,毕业后我是没有在实际工作中使用过,不过它既然能排名这么高,肯定还是有人用的。只是我没接触到。

后来我查了一下资料,VB竟然曾经排名第一!难怪当年计算机二级考试大都选的是VB,当时py和go都还没火起来。很多老的系统都是VB写的,尤其是客户端工具,同时很多ERP系统也是VB写的,尤其是日本用vb的非常多。在国内用VB的人就很少了。日本的web发展没有国内好,但客户端做的不错。日本很注重实业,他们很多ERP系统还是VB写的老系统。也有用SAP中ABAP编写的ERP系统。VB在中国的市场太小了。

现在知道VB为什么能排名这么高了吧!相当年我学习过的VB,竟然是排名第一的语言。现在它主要面向的是对日开发和开发windows小工具的利器。在国内市场很小。

C

后来学的c,主要是做嵌入式方面的,偏硬件,51、stm单片机,机器人编程(不是乐高机器人)这些,还参加了中国机器人大赛,毕业一年后想考浙大的研究生(最后迫于现实,还是工作了),又把c拾起来了,用c刷算法。

Java

再后来接触了java,当时学的还是不错的,用java刷算法题,那时比较推崇java,16年到17年java还不像现在这么卷,我从某兴离职,去了某宁后,当时的项目经理带我用了py,从此一发不可收拾,随着工作的不断深入,真真切切体会到,python简直太方便了!果断放弃了java。

Python

到目前为止,我一直用py居多,写过爬虫,写过自动化测试,写过自动化运维脚本,还参与了云平台后端的部分研发工作等等,真的是在工作中逐渐放飞自己,python真的是太好用了!

Go

上面是我的编程语言学习过程,VB→C→JAVA→PYTHON→GO 随着接触的越多,发现自己的知识广度足够了,甚至甩出同龄人几条街,可精度不够,说到这,我想到大学毕业答辩给我打的评语: 知识面较广,语言表达能力一般,逻辑思维一般

编程语言只是工具,每次去南京图书馆,计算机书架上的书,那么多编程语言琳琅满目,看得我有点不知所措,在软件行业也工作了不少年,发现自己没精通一门语言,一直很“肤浅”在学习,接下来的时间里,打算只选择一门语言作为主要语言,也是未来工作中的语言。我选了Go,辅助语言选了py。精力毕竟是有限的。我认同了Go这门语言,就要适应它的设计思想编程规范语法特点等等。

2. 为什么学习go

对初学者足够友善,能够快速上手
学了一段时间了,相对C,Java之类,Go很简化而灵活, 还需要在不断实践打磨自己的编程水平
生产力与性能的最佳结合

Go在性能方面,没得说,看了官方的数据还有别人跑的对比,以后随着学习和工作的深入,慢慢体会吧。 Go 能够在保持生产力的同时,大幅度提高性能。比如,

  • 全球知名的非营利教育组织可汗学院从 2019 年末开始,就将其在线教育平台的实现从 Python 迁移到了 Go。虽然 Go 代码行数要多于 Python,但他们收获了近 10 倍的性能提升。
  • 同样,知乎也是,后端由py迁移到了Go,节省了大量服务器。
快乐又有前景
  • 个人兴趣,自接触go,看到了跟以前不一样的世界,学py的时候没做到的,没学到的,用go做到了。
  • 想选一门语言深入下去,不想那么杂。精通一门,我比较感兴趣的,Go和Python,而Py虽然我在工作中的多,但还是希望自己能够学习一门静态编译型语言,权衡之下,选了Go。

3. Go的设计哲学

简单显式组合并发面向工程

  • 简单是指 Go 语言特性始终保持在少且足够的水平,不走语言特性融合的道路,但又不乏生产力。简单是 Go 生产力的源泉,也是 Go 对开发者的最大吸引力;
  • 显式是指任何代码行为都需开发者明确知晓,不存在因“暗箱操作”而导致可维护性降低和不安全的结果;
  • 组合是构建 Go 程序骨架的主要方式,它可以大幅降低程序元素间的耦合,提高程序的可扩展性和灵活性;
  • 并发是 Go 敏锐地把握了 CPU 向多核方向发展这一趋势的结果,可以让开发人员在多核时代更容易写出充分利用系统资源、支持性能随 CPU 核数增加而自然提升的应用程序;
  • 面向工程是 Go 语言在语言设计上的一个重大创新,它将语言要解决的问题域扩展到那些原本并不是由编程语言去解决的领域,从而覆盖了更多开发者在开发过程遇到的“痛点”,为开发者提供了更好的使用体验。

设计哲学~组合

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

在诠释组合之前,我们需要先来了解一下 Go 在语法元素设计时,是如何为“组合”哲学的应用奠定基础的

在 Go 语言设计层面,Go 设计者为开发者们提供了正交的语法元素,以供后续组合使用,包括:

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

Go的这种设计还是很好的。Go里面没有类的概念,真的简单很多。也没有继承的概念。要搞清楚的,就是struct示例,interface,以及方法之间的组合。这个可以自己写个程序自己试一下,体会了才知道。

我们可以看到,无论是包、接口还是一个个具体的类型定义,Go 语言其实是为我们呈现了这样的一幅图景:一座座没有关联的“孤岛”,但每个岛内又都很精彩。那么现在摆在面前的工作,就是在这些孤岛之间以最适当的方式建立关联,并形成一个整体。而 Go 选择采用的组合方式,也是最主要的方式。

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

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

你可以看看下面这个 Go 标准库中的一段使用类型嵌入的组合方式的代码段:

1
2
3
4
5
6
7
// $GOROOT/src/sync/pool.go
type poolLocal struct {
    private interface{}   
    shared  []interface{}
    Mutex               
    pad     [128]byte  
}

在代码段中,我们在 poolLocal 这个结构体类型中嵌入了类型 Mutex,这就使得 poolLocal 这个类型具有了互斥同步的能力,我们可以通过 poolLocal 类型的变量,直接调用 Mutex 类型的方法 Lock 或 Unlock。

另外,我们在标准库中还会经常看到类似如下定义接口类型的代码段:

1
2
3
4
5
// $GOROOT/src/io/io.go
type ReadWriter interface {
    Reader
    Writer
}

这里,标准库通过嵌入接口类型的方式来实现接口行为的聚合,组成大接口,这种方式在标准库中尤为常用,并且已经成为了 Go 语言的一种惯用法。

垂直组合本质上是一种“能力继承”,采用嵌入方式定义的新类型继承了嵌入类型的能力。Go 还有一种常见的组合方式,叫水平组合。和垂直组合的能力继承不同,水平组合是一种能力委托(Delegate),我们通常使用接口类型来实现水平组合。

Go 语言中的接口是一个创新设计,它只是方法集合,并且它与实现者之间的关系无需通过显式关键字修饰,它让程序内部各部分之间的耦合降至最低,同时它也是连接程序各个部分之间“纽带”。

水平组合的模式有很多,比如一种常见方法就是,通过接受接口类型参数的普通函数进行组合,如以下代码段所示:

1
2
3
4
5
// $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 语言,在表现力上丝毫不逊色于其他复杂的主流编程语言。

设计哲学~并发

“并发”这个设计哲学的出现有它的背景,你也知道 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 语言也更适应现代计算环境。

设计哲学~面向工程

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

很多编程语言设计者和他们的粉丝们认为这些问题并不是一门编程语言应该去解决的,但 Go 语言的设计者并不这么看,他们在 Go 语言最初设计阶段就将解决工程问题作为 Go 的设计原则之一去考虑 Go 语法、工具链与标准库的设计,这也是 Go 与其他偏学院派、偏研究型的编程语言在设计思路上的一个重大差异。

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

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

设计哲学~简单

Go 语法层面上呈现了这样的状态:

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

看,我说的没错吧,确实挺简单的。当然了,任何的设计都存在着权衡与折中。我们看到 Go 设计者选择的“简单”,其实是站在巨人肩膀上,去除或优化了以往语言中,已经被开发者证明为体验不好或难以驾驭的语法元素和语言机制,并提出了自己的一些创新性的设计。比如,首字母大小写决定可见性、变量初始为类型零值、内置以 go 关键字实现的并发支持等。

Go 这种有些“逆潮流”的“简单哲学”并不是一开始就能得到程序员的理解的,但在真正使用 Go 之后,我们才能真正体会到这种简单所带来的收益:简单意味着可以使用更少的代码实现相同的功能;简单意味着代码具有更好的可读性,而可读性好的代码通常意味着更好的可维护性以及可靠性。

总之,在软件工程化的今天,这些都意味着对生产效率提升的极大促进,我们可以认为简单的设计哲学是 Go 生产力的源泉。

上面的设计哲学需要在实践中去体会,赶紧用项目练起来。

设计哲学~显式

首先,我想先带你来看一段 C 程序,我们一起来看看“隐式”代码的行为特征。在 C 语言中,下面这段代码可以正常编译并输出正确结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <stdio.h>

int main() {
    short int a = 5;

    int b = 8;
    long c = 0;
    
    c = a + b;
    printf("%ld\\n", c);
}

我们看到在上面这段代码中,变量 a、b 和 c 的类型均不相同,C 语言编译器在编译c = a + b这一行时,会自动将短整型变量 a 和整型变量 b,先转换为 long 类型然后相加,并将所得结果存储在 long 类型变量 c 中。那如果换成 Go 来实现这个计算会怎么样呢?我们先把上面的 C 程序转化成等价的 Go 代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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 进行显式转型,就像下面代码段中这样:

1
2
c = int64(a) + int64(b)
fmt.Printf("%d\\n", c)

而这其实就是 Go 语言显式设计哲学的一个体现。

在 Go 语言中,不同类型变量是不能在一起进行混合计算的,这是因为 Go 希望开发人员明确知道自己在做什么,这与 C 语言的“信任程序员”原则完全不同,因此你需要以显式的方式通过转型统一参与计算各个变量的类型。

除此之外,Go 设计者所崇尚的显式哲学还直接决定了 Go 语言错误处理的形态:Go 语言采用了显式的基于值比较的错误处理方案,函数 / 方法中的错误都会通过 return 语句显式地返回,并且通常调用者不能忽略对返回的错误的处理。

这种有悖于“主流语言潮流”的错误处理机制还一度让开发者诟病,社区也提出了多个新错误处理方案,但或多或少都包含隐式的成分,都被 Go 开发团队一一否决了,这也与显式的设计哲学不无关系。

4. gofmt

值得重点介绍的是 gofmt,它统一了 Go 语言的代码风格,在其他语言开发者还在为代码风格争论不休的时候,Go 开发者可以更加专注于领域业务中。同时,相同的代码风格让以往困扰开发者的代码阅读、理解和评审工作变得容易了很多,至少 Go 开发者再也不会有那种因代码风格的不同而产生的陌生感。Go 的这种统一代码风格思路也在开始影响着后续新编程语言的设计,并且一些现有的主流编程语言也在借鉴 Go 的一些设计。

5. 标准库

在标准库方面,Go 被称为“自带电池”的编程语言。如果说一门编程语言是“自带电池”,则说明这门语言标准库功能丰富,多数功能不需要依赖外部的第三方包或库,Go 语言恰恰就是这类编程语言。

由于诞生年代较晚,而且目标比较明确,Go 在标准库中提供了各类高质量且性能优良的功能包,其中的net/httpcryptoencoding等包充分迎合了云原生时代的关于 API/RPC Web 服务的构建需求,Go 开发者可以直接基于标准库提供的这些包实现一个满足生产要求的 API 服务,从而减少对外部第三方包或库的依赖,降低工程代码依赖管理的复杂性,也降低了开发人员学习第三方库的心理负担。所以还是推荐把标准库好好学一下,至于第三方库,学起来其实很快。而且每个第三库同一个类别的第三方库其实原理都差不多的。调用的底层方法也是差不多的。

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