《Go 语言编程》学习笔记

Herb Sutter 在2003年写的《免费午餐已经结束》,言犹在耳,尽量通过并发来压榨硬件性能是大势所趋。但是并发难写。

并发执行是有执行体的。process、thread 和 coroutine 都是执行体。线程是轻量级进程,协程是轻量级线程。但大多数语言在语法层面上并不支持创建协程,而通过库的方式支持的协程功能也不完善。Go 语言提供的协程叫 Goroutine。Go 语言标准库提供的所有系统调用(syscall)操作,当然也包括所有同步 IO 操作,都会出让 CPU 给其他 goroutine,这让事情变得非常简单(哪里简单了,它自动产生了 yield 吗?)。

执行体间的通信,包括几个方式:

  1. 执行体之间的互斥与同步
  2. 执行体之间的消息传递

执行体之间存在共享资源时,为保证内存访问逻辑的确定性,需要对访问该共享资源的相关执行体进行互斥。当多个执行体之间的逻辑存在时序上的依赖时,也往往需要在执行体之间进行同步。护持与同步是执行体间最基础的交互方式。

在并发编程模型的选择上,有两个流派,一个是共享内存模型,一个是消息传递模型。多数传统语言选择了前者,少数语言选择了后者。后者被称作“Erlang 风格的并发模型”。它包括:

  1. 轻量级的进程。
  2. 消息乃是进程间通信的唯一方式。当执行体之间需要相互传递消息时,通常需要基于一个消息队列或者一个进程邮箱这样的基础设施通信。

Go 内置了对消息队列的支持,在 Go 里消息队列叫 channel。

这本书认为,Go 是除了 Python 以外唯一可以多返回值的主流语言。实际上如果用 Tupple 返回算多返回值的话,Scala 也可以做到多返回值。如果存储过程里面的 out 关键字也算的话,SQL 也是多返回值的语言。

Go 是不支持类型继承和重载的,通过类型组合的方式来提供代码复用和多态的特点。它支持非侵入式的接口。

Go 的命令行工具天然能够理解工程依赖结构。自动决定生成的是包还是可执行文件。go 编译生成的可执行文件天然就是可以被 gdb 调试的。这点和C/C++等系统语言里编译要选调试版本,来让文件中包含调试头信息大相径庭。倒类似起 Java、Javascript、Ruby、Python 等拥有解释器,可以用调试模式启动的语言了。

值得一读的参考文献:

  1. 并发之痛

拾遗

GOPATH 问题

默认的GOPATH 是在~/go下。
安装的包在$GOPATH/src 下,所以很多包如果不能直接识别的话,要考虑加上 src 路径。
一般的 go binary files 都在$GOPATH/bin下。

默认的情况下,每个第三方包的最开始的前缀是开发这个包的组织名。用 go get 一个这样的包名,会去这个组织的网址下载特定的所有包结构,全部装到$GOPATH里。

包问题

一个文件夹下所有的文件属于同一个包。和 Java 等语言不同,Golang 的包名在声明的时候是不嵌套的,在引用的时候才嵌套。一个包的包名就等于最后一段文件夹名。同一个包内的所有符号都是可见的,大写字符开头的符号是包外也可见。引用包的时候倒是需要引用完整的相对于 GOPATH 的相对路径路径。例如,包gopl.io/ch1/helloworld对应的路径应该是$GOPATH/src/gopl.io/ch1/helloworld。注意,包路径的分隔符是斜杠而不是点号。这个路径的最后一个字段相同。因为包名通常是小写,所以引用起来就像一个util类型一样,可以认为包下面的各个文件是一个类型的各个实现部分。

每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。编译器会解决提供给它的 go 文件的编译顺序问题。对于包级别的变量,他们的声明顺序是无关紧要的,但可以通过一个特殊的 init() 函数来顺序初始化。每个文件可以包含多个 init() 函数,这些函数不能被调用和引用,但会被自动执行。

main 是一个特殊的包,它在解决了依赖包以后,最后被初始化。main 函数作为程序的入口,应该被放在 main 包里。

函数声明级别

包一级,函数一级。

作用域级别

全局级别,内置的符号。

文件级别,由 import 语句导入的符号。

包级别,函数外部定义的符号。

各种块级别,当然最重要的还是函数级别。

嵌套的内部作用域会 shadow 掉外部作用域。

这些都是词法作用域的分类。

数组与 Slice

数组是定长的,slice 是不定长的。slice 总是引用底层的一个数组,也就是说总是底层数组派生出来的。

slice 之间无法直接求等,必须深入展开求等。slice 可以和 nil 求等。

slice 的扩容会引起底层的拷贝。

方法

方法是与类型相关联的函数。

方法的接受者可以是一个类型,也可以是一个类型的指针。前者会导致对象的拷贝而后者不会。编译器会帮我们方便地 handle 调用的语法糖问题。

Golang 有个潜规则,就是使用如果一个类型有一个指针接受类型的方法,所有方法必须有指针接收的版本。

继承

直接通过组合来实现。