《Go语言精进之路——从新手到高手的编程思想、方法和技巧》白明/著

《Go语言精进之路——从新手到高手的编程思想、方法和技巧》白明/著

第一部分 熟知Go语言的一切

第1条 了解Go语言的诞生与演进

Go语言的诞生

Go语言的早期团队和演进历程

Go语言正式发布并开源

第2条 选择适当的Go语言版本

Go语言的先祖

Go语言的版本发布历史

Go语言的版本选择建议

第3条 理解Go语言的设计哲学

追求简单,少即是多

偏好组合,正交解耦

原生并发,轻量高效

面向工程,自带电池

第4条 使用Go语言原生编程思维来写Go代码

语言与思维——来自大师的观点

现实中的投影

Go语言原生编程思维

第二部分 项目结构、代码风格与标识符命名

第5条 使用得到公认且广泛使用的项目结构

Go项目的项目结构

Go语言典型项目结构

第6条 提交前使用gofmt格式化源码

gofmt:Go语言在解决规模化问题上的最佳实践

使用gofmt

使用goimports

将gofmt/goimports与IDE或编辑器工具集成

第7条 使用Go命名惯例对标识符进行命名

简单且一致

利用上下文环境,让最短的名字携带足够的信息

第三部分 声明、类型、语句与控制结构

第8条 使用一致的变量声明形式

包级变量的声明形式

局部变量的声明形式

第9条 使用无类型变量简化代码

Go常量溯源

有类型常量带来的烦恼

无类型常量消除烦恼,简化代码

第10条 使用iota实现枚举常量

第11条 尽量定义零值可用的类型

Go类型的零值

零值可用

第12条 使用复合字面值作为初值构造器

结构体复合字面量

数组/切片复合字面量

map复合字面量

第13条 了解切片实现原理并高效使用

切片究竟是什么

切片的高级特性:动态扩容

尽量使用cap参数创建切片

第14条 了解map实现原理并高效使用

什么是map

map的基本操作

map的内部实现

尽量使用cap参数创建map

第15条 了解string实现原理并高效使用

Go语言的字符串类型

字符串的内部表示

字符串的高效构造

字符串相关的高效转换

第16条 理解Go语言的包导入

Go程序构建过程

究竟是路径名还是包名

包名冲突问题

第17条 理解Go语言表达式的求值顺序

包级别变量声明语句中的表达式

普通求值顺序

赋值语句的求值

switch/select语句中的表达式求值

第18条 理解Go语言代码块与作用域

Go代码块与作用域简介

if条件控制语句的代码块

其他控制语句的代码块规则简介

第19条 了解Go语言控制语句惯用法及使用注意事项

使用if控制语句时应遵循"快乐"

for range 的避坑指南

break跳到哪里去了

尽量用case表达式列表替代fallthrough

第四部分 函数与方法

第20条 在init函数中检查包级变量的初始状态

认识init函数

程序初始化顺序

使用init函数检查包级变量的初始状态

第21条 让自己习惯于函数是一等公民

什么是一等公民

函数作为一等公民的特殊运用

-> 第22条 使用defer让函数更简洁、更健壮

​ defer的作用:

  • 在函数、方法中使用defer,可以减轻开发人员的心智负担。比如,处理待释放的资源;
  • 即使函数、方法中发生panic(除了一些运行时致命问题外),defer都可以被执行到。
  • 降低函数、方法中的逻辑复杂度,增加程序可读性,进而提高健壮性。

defer的运作机制

  • 只有在函数、方法中才可以使用defer;

  • defer这一关键字后,也只能接上函数(被称为 deferred函数)、方法(被称为 deferred方法);

  • deferred函数或方法,【在defer关键字所在的函数或方法退出前】按后进先出(LIFO:Last In First Out)顺序调度执行。

defer的常见用法

  • 释放资源(最常见的用法);
  • 拦截panic(配合recover()函数一起使用),如:$GOROOT/src/bytes/buffer.go ;
  • 修改函数的具名返回值,如:$GOROOT/src/fmt/scan.go;
  • 输出调试信息,如:$GOROOT/src/net/conf.go -> hostLookupOrder;
  • 还原变量的旧值,如:$GOROOT/src/syscall/fs_nacl.go -> init。

关于defer的几个关键问题

(a)须知哪些函数、方法可以作为deferred函数、方法;

  • 若deferred函数、方法,是有返回值的自定义函数、方法,则在defferred函数、方法被调度执行时该返回值被自动丢弃
  • 虽然对于15个内置函数中,close、copy、delete、panic、print、println、recover可以直接接在defer关键字之后,而append、cap、complex、imag、len、make、new、real不可以。但不妨还是直接在defer关键字之后接上一个匿名函数进行包裹处理,减少心智负担。

(b)须知defer关键字后表达式的求值时机;

(c)须知defer会带来性能损耗。

第23条 理解方法的本质以选择正确的receiver类型

方法的本质

选择正确的receiver类型

基于对Go方法本质的理解巧解难题

第24条 方法集合决定接口实现

方法集合

类型嵌入与方法集合

defined类型的方法集合

类型别名的方法集合

第25条 了解变长参数函数的妙用

什么是变长参数函数

模拟函数重载

模拟实现函数的可选参数与默认参数

实现功能选项模式

第五部分 接口

第26条 了解接口类型变量的内部表示

nil error值 != nil

接口类型变量的内部表示

输出类型变量内部表示的详细信息

接口类型的装箱原理

第27条 尽量定义小接口

Go推荐定义小接口

小接口的优势

定义小接口可以遵循的一些点

第28条 尽量避免使用空接口作为函数参数类型

第29条 使用接口作为程序水平组合的连接点

一切皆组合

垂直组合问题

以接口为连接点的水平组合

第30条 使用接口提高代码的可测试性

实现一个附加免责声明的电子邮件发送函数

使用接口来减低耦合

第六部分 并发编程

-> 第31条 优先考虑并发设计

并发(concurrency )不是并行(parallelism),并发关乎结构,并行关乎执行。—— Rob Pike

并发与并行

Go并发设计实例

第32条 了解goroutine的调度原理

goroutine调度器

goroutine调度模型与演进历程

对goroutine调度器原理的进一步理解

调度器状态的查看方法

goroutine调度实例简要分析

第33条 掌握Go并发模型和常见并发模式

Go并发模型

Go常见的并发模式

第34条 了解channel的妙用

无缓冲channel

带缓冲channel

nil channel的妙用

与select结合使用的一些惯用法

第35条 了解sync包的正确用法

sync包还是channel

使用sync包的注意事项

互斥锁还是读写锁

条件变量

使用sync.Once实现单例模式

使用sync.Pool减轻垃圾回收压力

第36条 使用atomic包实现伸缩性更好的并发读取

atomic包对原子操作

对共享整型变量的无锁读写

对共享自定义类型变量的无锁读写

第七部分 错误处理

-> 第37条 了解错误处理的4种策略

​ 从go语言被人所诟病的、似乎有些过时的错误处理机制作为开章语。

​ 进而解释,之所以没有像主流编程语言那样提供基于异常的结构化try-catch-finally错误处理机制,是因为个go的设计者有两点不同的看法:

  1. 将异常耦合到程序控制结构中会导致代码混乱;
  2. (更进一步)若提供了try-catch-finally错误处理机制,程序员将会把大多数常见错误(如无法打开文件等)标记为异常,如此则背离了Go所追求的简单价值观。

​ 再抛出,现在Go的错误处理机制:基于错误值比较的错误处理机制

​ 其借鉴了C语言的经典错误机制:错误就是值。但又因go天生支持多返回值机制(惯用法是将err作为最后一个返回值),避免了C语言的函数返回值需承载多重信息的问题(例如C语言中的 fprintf函数,错误时返回一个负数,正常时返回输出到FILE流中的字符数)。

​ 最后给出这样的错误处理机制的好处:

  • 虽开发人员须显示关注和处理每个错误,但这样处理会使代码更健壮;
  • 更容易调试代码;
  • 可针对每个错误处理的决策分支进行测试覆盖;
  • 代码可读性更佳;

构造错误值

​ error接口是go原生内置的类型,不需要使用import引入即可使用。

​ go中提供了:error.New()和fmt.Errorf()这两个函数来构造错误值。

​ go 1.13(不含)之前,error.New()和fmt.Errorf()返回的错误值的类型是errors.errorString这个底层类型。

​ 但在go 1.13及之后,若fmt.Errorf(“格式化字符串”)的格式化字符串中使用一个%w,则fmt.Errorf()返回的错误值的类型是fmt.wrapError这个底层类型;若使用多个%w,则fmt.Errorf()返回的错误值的类型是fmt.wrapErrors这个底层类型。请参阅fmt.Errorf()

​ 另外需要注意:

​ 虽然fmt.wrapErrors和fmt.wrapError在源码中都有实现Unwrap()方法,但fmt.wrapErrors和fmt.wrapError是不可导出的类型,故不能在自己的代码中使用到Unwrap()方法。

​ 虽然我们不能调用到Unwrap()方法,但可以通过errors.Unwrap(err变量)的方式来间接调用到Unwrap() 方法。可惜的是,您会发现只有当err变量是fmt.wrapError类型,errors.Unwrap(err变量)才能间接调用Unwrap()方法的结果;若是err变量是fmt.wrapErrors类型,errors.Unwrap(err变量)输出为nil。具体可参阅errors.Unwrap

遗留问题,那fmt.wrapErrors实现的Unwrap() 方法有什么作用?哪里可以用得到?

​ 您会发现,无论是error.New()还是fmt.Errorf()构造的错误值,都只是以字符串形式呈现给错误处理者。

​ 若需要更多数据信息提供给错误处理者,可以通过自定义错误类型(需实现error接口)来满足。

透明错误处理策略

​ 最简单的错误处理策略:不用关心返回错误值携带的上下文信息,只要发生错误直接使用类似if err != nil { ... return err }的形式进行错误处理。

​ go语言中**80%**的错误处理情形,都是这种最简单的错误处理策略。

​ 因错误处理方不关心错误值的上下文,且错误构造方直接使用error.New()或fmt.Errorf()来构造错误值,这种策略下构造出来的错误值,对错误处理方是透明的,故也称这种策略是"透明错误处理策略"。

“哨兵"错误处理策略

​ 若不能直接使用透明错误处理策略,则错误处理方可能会对错误构造方给出的错误值进行不同的判断处理,例如使用switch...case 语句。

​ 若直接根据透明错误值的字符串(err.Error()返回的值)进行判断,会带来严重的隐式耦合

​ 那怎么办?

​ go标准库中采用定义导出的哨兵错误值的方式,方便错误处理方对错误构造方给出的透明错误值进行判断处理。

​ 哨兵错误值变量以ErrXXX格式命名。对于API的开发者而言,这些哨兵错误值变量,也是API的一部分,需要进行很好的维护。

​ go 1.13及之后的版本,可以使用errors.Is(err, 某一哨兵错误值变量),来判断错误值。相比于之前使用的if err == 某一哨兵错误值变量 { ... },若err是fmt.wrapErrorfmt.wrapErrors类型,errors.Is()方法会在错误链上找到第一个匹配的错误,匹配则返回true。请参阅errors.Is()这也是目前推荐的。

​ go源码:$GOROOT/src/bufio/bufio.go 采用了这种策略。

错误值类型检视策略

​ 以上两种错误处理策略(哨兵错误处理策略、最简错误处理策略)都不能提供更多的错误上下文(都只是直接提供了透明错误值,只能从中得到字符串形式的错误描述)。

​ 若错误构造方想提供更多的错误上下文,则需要自定义错误类型(须实现error接口,一般以XXXError形式命名)。如此,错误处理方则需要通过先判断错误类型,再对不同错误类型进行分别处理。

​ 错误处理方可以使用go提供的类型断言机制、类型选择机制来判断和处理不同错误类型。类似这样的错误处理,作者认为是错误值类型检视策略

​ go源码:$GOROOT/src/encoding/json/decode_test.go 中使用了类型断言机制来处理错误。

​ go源码:$GOROOT/src/encoding/json/decode.go 中使用了类型选择机制来处理错误。

​ 因自定义错误类型,需暴露给错误处理方,故类似哨兵错误值变量,都需要对其进行很好的维护。

​ 从go1.13及之后可以使用errors.As(err, &自定义错误类型变量) 函数,进行错误类型判断,以及进一步处理。相比于之前使用的if e, ok := err.(*MyError); ok { ... },若err是fmt.wrapErrorfmt.wrapErrors类型,errors.As()方法会在错误链上找到第一个匹配的错误,匹配则返回true。请参阅errors.As()这也是目前推荐的。

错误行为特性检视策略

​ 什么是错误行为特性检视策略?

​ 可以认为是在自定义错误类型中,定义了需要实现的方法(一般是表示行为的方法,例如是否超时、是否是临时性错误),错误构造方在提供错误值的时候,会对这些实现的方法体中需要用到的一些数据进行设置,如此在当错误处理方,在判断到是这种自定义错误类型时,可以调用该错误类型实现的方法,进而获取到一定的数据(一般是布尔类型值),以此达到错误处理的目的。

​ go源码:$GOROOT/src/net/server.go 使用了这种策略。

第38条 尽量优化反复出现的if err != nil

​ 因go选择使用显式错误结果和显式错误检查。由此带来可能需要在go代码中反复出现类似if err != nil的错误检查。

​ 以CopyFile函数中过多的if err != nil 作为开章。

两种观点

​ 观点1:急需改善go错误处理方式,例如引入try。

​ 观点2:go的成功很大程度上要归功于显式的错误处理方式,与其花费成本在故障发生时处理故障,不如花费成本在反复出现的代码片段if err != nil {...}的优化上。

尽量优化

​ Russ Cox也认为当前go错误处理机制对于go开发人员来说确实有一定的心智负担。

​ 对于反复出现的if err != nil 还是需要尽可能去优化!

优化思路

​ 可以从两个方向来优化:

  • 改善代码中的视觉呈现;
  • 降低if err != nil重复的次数(即降低函数或方法的复杂度,可采用进一步封装、分层)。

第39条 不要使用panic进行正常的错误处理

Go的panic不是Java的checked exception

panic的典型应用

理解panic的输出信息

第八部分 测试、性能剖析与调试

第40条 理解包内测试与包外测试的差别

官方文档的"自相矛盾”

包内测试与包外测试

第41条 有层次地组织测试代码

经典模式——平铺

xUnit家族模式

测试固件

第42条 优先编写表驱动的测试

Go测试代码的一般逻辑

表驱动的测试实践

表驱动测试的优点

表驱动测试实践中的注意事项

第43条 使用testdata管理测试依赖的外部数据文件

testdata目录

golden文件惯用法

第44条 正确运用fake、stub和mock等辅助单元测试

fake:真实组件或服务的简化实现版替身

stub:对返回结果有一定预设控制能力的替身

mock:专用于行为观察和验证的替身

第45条 使用模糊测试让潜在bug无处遁形

模糊测试在挖掘Go代码的潜在bug中的作用

go-fuzz的初步工作原理

go-fuzz使用方法

使用go-fuzz建立模糊测试的示例

让模糊测试称为一等公民

第46条 为被测对象建立性能基准

性能基准测试在Go语言中是一等公民

顺序执行和并行执行的性能基准测试

使用性能基准比较工具

排除额外干扰,让基准测试更精确

第47条 使用pprof对程序进行性能剖析

pprof的工作原理

使用pprof进行性能剖析的实例

第48条 使用expvar输出度量数据,辅助定位性能瓶颈点

expvar包的工作原理

自定义应用通过expvar输出的度量数据

输出数据的展示

第49条 使用Delve调试Go代码

关于调试,您首先应该知道的几件事

Go调试工具的选择

Delve调试基础、原理与架构

并发、Coredump文件与挂接进程调试

第九部分 标准库、反射与cgo

第50条 理解Go TCP Socket网络编程模型

TCP Socket网络编程模型

TCP连接的建立

Socket读写

Socket属性

关闭连接

第51条 使用net/http包实现安全通信

HTTPS:在安全传输层上运行的HTTP协议

HTTPS安全传输层的工作机制

非对称加密和公钥证书

对服务端公钥证书的校验

对客户端公钥证书的校验

第52条 掌握字符集的原理和字符编码方案间的转换

字符与字符集

Unicode字符集的诞生与UTF-8编码方案

字符编码方案间的转换

第53条 掌握使用time包的正确方式

时间的基础操作

时间的格式化输出

定时器的使用

第54条 不要忽视对系统信号的处理

为什么不能忽视对系统信号的处理

Go语言对系统信号处理的支持

使用系统信号实现程序的优雅退出

第55条 使用crypto下的密码学包构建安全应用

Go密码学包概览与设计原则

分组密码算法

公钥密码

单向散列函数

消息认证码

数字签名

随机数生成

第56条 掌握bytes包和strings包的基本操作

查找与替换

比较

分割

拼接

修剪与变换

快速对接I/O模型

第57条 理解标准库的读写模型

直接读写字节序列

直接读写抽象数据类型实例

通过包裹类型读写数据

第58条 掌握unsafe包的安全使用模式

简洁的unsafe包

unsafe包的典型应用

正确理解unsafe.Pointer与uintptr

unsafe.Pointer的安全使用模式

第59条 谨慎使用reflect包提供的反射能力

Go反射的三大法则

反射世界的入口

反射世界的出口

输出参数、interface{}类型变量及反射对象的可设置性

第60条 了解cgo的原理和使用开销

Go调用C代码的原理

在Go中使用C语言的类型

在Go中链接外部C库

在C中使用Go函数

使用cgo的开销

使用cgo代码的静态构建

第十部分 工具链与工程实践

第61条 使用module管理包依赖

Go语言包管理演进回顾

Go module:Go包依赖管理的生产标准

Go module 代理

升级module的主版本号

第62条 构建最小Go程序容器镜像

镜像:继承中的创新

镜像是个筐:初学者的认知

理性回归:builder模式的崛起

像赛车那样减重:追求最小镜像

要有光:对多阶层构建的支持

第63条 自定义Go包的导入路径

govanityurls

使用govanityurls

第64条 熟练掌握Go常用工具

获取与安装

包或module检视

构建

进行与诊断

格式化与静态代码检查

重构

查看文档

代码导航与洞察

第65条 使用go generate驱动代码生成

go generate:Go原生的代码生成"驱动器"

go generate的工作原理

go generate的应用场景

第66条 牢记Go的常见陷阱

语法规范类

标准库类

最后修改 October 10, 2024: 更新 (a4b8f85)