目录

Go 语言进阶开篇

1. Go 语言进阶

前面 Go语言入门中我们学习了 Go 语言的基础语法和使用。算起来已经看过好几本 Go 的书籍了,但始终感觉"不得其法",比如一直不明白reflect 库是如何实现 Go 语言变量自省的、Go 实现的http 服务底层有没有使用IO多路复用等等。直到今年双十一囤了《Go 语言精进之路》系列的两本书,花了一个月时间快速了看了一遍,收获真的很大。我感觉对于每个像我这样普通的程序员,学习一门语言总需要找到这样一本书,看完就感觉有种豁然开朗的感觉。所以这个系列我们来学习 《Go 语言精进之路》,作为 Go 语言的进阶。整个内容分成以下几个大的部分:

  1. Go 语言的语法进阶
  2. 测试与性能剖析
  3. 反射与 cgo
  4. 工具链与工程实践

我们正式进入学习之前,我们再来回顾一次 Go 语言的设计哲学。

2. Go语言的设计哲学

2.1 追求简单,少即是多

Go 语言的语法简单这一说法,是我最近学习 Rust 体会最深的一点。相比于 Rust,Go 语言真实简单太多了。

首先 Go 语言不支持传统的面向对象,所以也就不用去理解面向对象里面的那一套,继承、方法重载一堆概念。 与继承相比,Go 语言更推崇组合的设计哲学。在设计上类型嵌入为类型提供垂直扩展能力,interface是水平组合的关键:

  1. 类型嵌入(type embedding)有些类似经典OO语言中的继承机制,但在原理上与其完全不同,这是一种Go设计者们精心设计的语法糖。被嵌入的类型和新类型之间没有任何关系,甚至相互完全不知道对方的存在。在通过新类型实例调用方法时,方法的匹配取决于方法名字,而不是类型
  2. interface 只是方法集合,且与实现者之间的关系是隐式的。隐式的interface实现会不经意间满足依赖抽象、里氏替换、接口隔离等设计原则,通过interface将程序各个部分组合在一起的方法,实现水平组合

2.2 原生并发,轻量高效

goroutine 和 channel 直接拔高了 Go 在并发处理上的高度。由于goroutine的开销很小(相对线程),Go官方鼓励大家使用goroutine来充分利用多核资源。并发是有关结构的,它是一种将一个程序分解成多个小片段并且每个小片段都可以独立执行的程序设计方法;并发程序的小片段之间一般存在通信联系并且通过通信相互协作。并发与组合的哲学是一脉相承的,并发是一个更大的组合的概念,它在程序设计层面对程序进行拆解组合,再映射到程序执行层面:goroutine各自执行特定的工作,通过channel+select将goroutine组合连接起来。并发的存在鼓励程序员在程序设计时进行独立计算的分解,而对并发的原生支持让Go语言更适应现代计算环境。

2.3 面向工程,“自带电池”

相比于其他语言,最起码相比于我比较熟悉的 Python,Go 语言的工具链真的是相当完善了。 官方工具链,涵盖了编译、编辑、依赖获取、调试、测试、文档、性能剖析等的方方面面。

  1. 构建和运行:go build/go run
  2. 依赖包查看与获取:go list/go get/go mod xx
  3. 编辑辅助格式化:go fmt/gofmt
  4. 文档查看:go doc/godoc
  5. 单元测试/基准测试/测试覆盖率:go test
  6. 代码静态分析:go vet
  7. 性能剖析与跟踪结果查看:go tool pprof/go tool trace
  8. 升级到新Go版本API的辅助工具:go tool fix
  9. 报告Go语言bug:go bug
  10. 值得重点提及的是gofmt统一了Go语言的编码风格(好处懂的很懂)

在提供丰富的工具链的同时,Go语言的语法、包依赖系统以及命名惯例的设计也让针对Go的工具更容易编写,并且Go在标准库中提供了官方的词法分析器、语法解析器和类型检查器相关包,开发者可以基于这些包快速构建并扩展Go工具链。

Go设计者将所有工程问题浓缩为一个词:scale。从Go1开始,Go的设计目标就是帮助开发者更容易、更高效地管理两类规模。

  1. 生产规模:用Go构建的软件系统的并发规模,比如这类系统并发关注点的数量、处理数据的量级、同时并发与之交互的服务的数量等。
  2. 开发规模:包括开发团队的代码库的大小,参与开发、相互协作的工程师的人数等。

为了有效管理规模,Go 语言在语法设计等各个层面,故意做了一些限制。比如:

  1. 如果源文件导入了它不使用的包,则程序将无法编译。
  2. 故意不支持默认函数参数。因为在规模工程中,很多开发者利用默认函数参数机制向函数添加过多的参数以弥补函数API的设计缺陷,这会导致函数拥有太多的参数,降低清晰度和可读性。
  3. 首字母大小写定义标识符可见性,这是Go的一个创新。它让开发人员通过名称即可知晓其可见性,等于变现的要求程序对外定义暴露的接口。

除了工具链,Go 语言还有丰富的标准库。Go团队还在golang.org/x路径下提供了暂未放入标准库的扩展库/补充库供广大Gopher使用,包括text、net、crypto等。这些库的质量也是非常高的,标准库中部分包也将golang.org/x下的text、net和crypto包作为依赖包放在标准库的vendor目录中。

3. Go 语言典型目录结构

项目的目录结构,我个人觉得是存在一些最佳实践的,并且一个公司内部,项目结构最好保持一致,不然CI/CD 很难做。当然不同场景下,项目结构的设计是有一定差异的。

3.1 以构建二进制可执行文件为目的的Go项目结构

/images/go/expert/project_bin.png

  1. cmd目录:存放项目要构建的可执行文件对应的main包的源文件。如果有多个可执行文件需要构建,则将每个可执行文件的main包单独放在一个子目录中,比如图中的app1、app2
  2. pkg目录:存放项目自身要使用并且同样也是可执行文件对应main包要依赖的库文件。该目录下的包可以被外部项目引用,算是项目导出包的一个聚合。
  3. Makefile:这里的Makefile是项目构建工具所用脚本的“代表”;对于构建脚本较多的项目,也可以建立build目录,并将构建脚本的规则属性文件、子构建脚本放入其中。

3.2 以只构建库为目的的Go项目结构

/images/go/expert/project_package.png

这种结构去除了cmd和pkg两个子目录:由于仅构建库,没必要保留存放二进制文件main包源文件的cmd目录;由于Go库项目的初衷一般都是对外部(开源或组织内部公开)暴露API,因此也没有必要将其单独聚合到pkg目录下面了。

3.3 internal 目录

无论是上面哪种类型的Go项目,对于不想暴露给外部引用,仅限项目内部使用的包,在项目结构上可以通过Go 1.4版本中引入的internal包机制来实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 带internal的Go库项目结构

$tree -F ./chapter2/sources/GoLibProj
GoLibProj
├── LICENSE
├── Makefile
├── README.md
├── go.mod
├── internal/
│  ├── ilib1/
│  └── ilib2/
├── lib.go
├── lib1/
│  └── lib1.go
└── lib2/
      └── lib2.g

这样,根据Go internal机制的作用原理,internal目录下的ilib1、ilib2可以被以GoLibProj目录为根目录的其他目录下的代码(比如lib.go、lib1/lib1.go等)所导入和使用,但是却不可以为GoLibProj目录以外的代码所使用,从而实现选择性地暴露API包。当然internal也可以放在项目结构中的任一目录层级中,关键是项目结构设计人员明确哪些要暴露到外层代码,哪些仅用于同级目录或子目录中。对于以构建二进制可执行文件类型为目的的项目,我们同样可以将不想暴露给外面的包聚合到项目顶层路径下的internal下,与暴露给外部的包的聚合目录pkg遥相呼应。

关于 Go 语言项目结构的更多讨论,可以参考golang-standards 这个git项目

4. Go命名惯例

要想做好Go标识符的命名(包括对包的命名),至少要遵循两个原则:简单且一致;利用上下文辅助命名:

  1. 对于Go中的包(package),一般建议以小写形式的单个单词命名
  2. 保持简短命名变量含义上的一致性
  3. 对于接口类型优先以单个单词命名。对于拥有唯一方法(method)或通过多个拥有唯一方法的接口组合而成的接口,Go语言的惯例是用“方法名+er”命名。