这是我参与「第三届青训营 -后端场」笔记创作活动的第2篇笔记
本文主要内容是对高质量编程与性能调优实战课程的内容整理及部分课后习题的解答
编码规范
错误和异常处理
-
简单错误
- 优先使用
errors.New来创建匿名变量直接表示简单错误 - 如果有格式化的需求,使用
fmt.Errof
- 优先使用
-
错误的
Wrap和Unwrap- 错误的
Wrap实际上提供了一个error嵌套另一个error的能力,从而生成一个error的跟踪链 - 在
fmt.Errof中使用%w关键字来将一个错误关联至错误链中
- 错误的
-
错误判定
- 判定一个错误是否为特定错误,使用
errors.Is - 不同于使用
==,使用该方法可以判定错误链上的所有错误是否含有特定的错误 - 在错误链上获取特定种类的错误,使用
errors.As
- 判定一个错误是否为特定错误,使用
性能优化
Benchmark
- 性能表现需要实际数据衡量
- Go 语言提供了支持基准性能测试的 benchmark 工具
go test -bench=. -benchmem
以计算斐波拉契数列为例,分两个文件,fib.go 编写函数代码;fib_test.go 编写 benchmark 的逻辑, 命令行运行 benchmark 得到测试结果, benchmem表示也统计内存信息
Slice
-
尽可能在使用
make()初始化切片时提供容量信息,特别是在追加切片时 -
陷阱:大内存未释放
-
场景:
-
原切片较大,代码在原切片基础上新建小切片(在已有切片基础上创建切片,不会创建新的底层数组)
-
原底层数组在内存中有引用,得不到释放
- 可使用 copy 替代 re-slice
-
Map
-
预分配内存
- 不断向 map 中添加元素会触发 map 的扩容
- 提前分配好空间可以减少内存拷贝和 Rehash 的消耗
字符串处理
-
常见的字符串拼接方式
- 使用
+拼接性能最差,strings.Builder,bytes.Buffer相近,strings.Builder更快 - 使用
+拼接每次都会重新分配新的内存,大小是原来两个字符串大小之和 strings.Builder,bytes.Buffer底层都是[]byte数组,不需要每次重新分配内存bytes.Buffer转化为字符串时重新申请了一块内存空间,strings.Builder直接将底层的[]byte转换成了字符串类型返回strings.Builder,bytes.Buffer同样可以使用Grow方法实现内存预分配,进一步提升拼接性能
- 使用
空结构体
-
空结构体
struct{}实例不占据任何的内存空间 -
可作为各种场景下的占位符使用
- 节省资源
- 空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符
atomic包
- 多线程编程环境下
- 锁的实现通过操作系统实现,属于系统调用
atomic操作是通过硬件实现,效率比锁高sync.Mutex应该用来保护一段逻辑,不仅仅用于保护一个变量
性能分析工具——pprof
功能简介
pprof的采样结果是将一段时间内的信息汇总输出到文件中
CPU
go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
-
top命令展示的参数:
flat:当前函数本身的执行耗时flat%:flat占 CPU 总时间的比例sum%:上面每一行的flat%总和cum:指当前函数加上其调用函数的总耗时cum%:cum占 CPU 总时间的比例flat == cum说明函数中没有调用其他函数flat == 0说明函数中只有其他函数的调用
Heap堆内存
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
这里使用另一种展示方法,通过 -http=:8080 参数,可以开启 pprof 自带的 Web UI,性能指标会以网页形式呈现
-
打开 sample 菜单,会发现堆内存实际上提供了四种指标
-
默认展示的是
inuse_space视图,只展示当前持有的内存,但如果有的内存已经释放,这时inuse采样就不会展示了alloc_objects:程序累积申请的对象数alloc_space:程序累积申请的内存大小inuse_objects:程序当前持有的对象数inuse_space:程序当前占用的内存大小
-
切换到
alloc_space视图,继续分析下alloc的内存问题
goroutine
-
goroutine泄露也会导致内存泄露 -
Golang 是一门自带垃圾回收的语言,一般情况下内存泄漏没那么容易发生
-
但是有一种例外:
goroutine是很容易泄露的,进而会导致内存泄露 -
由于 pprof 生成了一张非常长的调用关系图,因此在此使用火焰图来更加直观地展示
- 火焰图自顶向下展示了各个调用,表示各个函数调用之间的层级关系
- 每一行中,条形越长代表消耗的资源占比越多
- 火焰图是动态的,支持点击块进行分析
pprof采样过程和原理
CPU
- CPU采样会记录所有的调用栈和它们的占用时间
- 进程会每秒暂停100次,每次记录当前的调用栈信息。汇总之后,根据调用栈在采样中出现的次数来推断函数的运行时间
- 启动采样时,进程向OS注册一个定时器,OS每隔10ms向进程发送一个 SIGPROF 信号,进程接收到信号后就会对当前的调用栈进行记录
- 与此同时,进程会启动一个写缓冲的goroutine,它会每隔100ms从进程中读取已经记录的堆栈信息,并写入到输出流
- 当采样停止时,进程向OS取消定时器,不再接收信号;写缓冲读取不到新的堆栈时,结束输出
Heap堆内存
- 内存采样在实现上依赖了内存分配器的记录,所以它只能记录在堆上分配,且会参与 GC 的内存
goroutine协程 & ThreadCreate线程创建
- 两者的采样是立刻触发的全量记录
Block阻塞 & Mutex锁
- 两者在实现上是基本相同的,都是一个主动上报的过程
课后习题
1. 了解下其他语言的编码规范,是否和 Go 语言编码规范有相通之处,注重理解哪些共同点?
编程语言的共通编码规范有很多:
- 在变量和函数的命名时需要满足最小化长度 && 最大化信息量原则:在尽量简洁的同时携带更多的上下文信息,让开发者能够轻易辨认出变量或者函数的含义
- 注释层面应详细解释代码的具体作用,输入输出等关键参数的具体含义,代码在什么情况下会出错,甚至可以在注释部分给出代码的某些具体示例
- 编码层面注意整体代码结构的清晰与可读性,避免冗余的嵌套,优先处理错误与特殊情况(尽早返回)
2. 编码规范或者性能优化建议大部分是通用的,有没有方式能够自动化对代码进行检测?
- Go语言官方提供了
gofmt和goimports等工具,前者可以自动格式化 Go 语言代码,使得团队开发过程中基本的代码风格能保持统一,便于阅读;后者提供了对于依赖包的管理,能够自动增删依赖的包引用 - Go 语言标准库的
testing包提供了功能性测试和压力测试常用的方法的框架,也可以非常方便地利用其进行自动化测试
3. 使用 Go 进行并发编程时有哪些性能陷阱或者优化手段?
-
在并发编程时可能会遇到并发访问共享数据的错误,可能同时存在多个
goroutine读取到了共享数据并对其进行操作,而造成了相互覆盖的情况。可以使用 Go 语言提供的go run -race来发现该问题 -
当我们使用
for-loop创建并发起多个goroutine时,整个程序可以分为三步:- 先创建:
for-loop循环创建goroutine - 再调度:协程
goroutine开始调度执行 - 才执行:开始执行
goroutine内的逻辑
因此创建
goroutine与真正执行goroutine内的逻辑可能并不同步 - 先创建:
4. Go 语言本身在持续更新迭代,每个版本在性能上有哪些重要的优化点?
| 版本 | 主要更新 |
|---|---|
| 1.0 | 修改 goroutine初始化方式:在初始化时就开始运行;新增 rune, error等类型;多线程调度器 |
| 1.1 | 允许函数不一定有return 返回;任务窃取调度器(引入了处理器P,构成了目前的GMP模型) |
| 1.2 | nil指针不允许访问内存地址;允许slice切片添加第三个参数 slice[a:b:c];基于协作的抢占式调度器 |
| 1.3 | 允许有缓存的channel;更改对操作系统的支持;更改垃圾回收器;map的迭代改为随机性 |
| 1.4 | 允许range不需要任何index和value;用 go 重写了 gc |
| 1.5 | 编译器和 runtime 不再有 c 代码;gc的算法发生变更;调度器的调度顺序发生变更;并发编译 |
| 1.6 | 支持http/2;增加了并发 map 结果的错误探测 |
| 1.7 | 明确语句的终止,为了方便 gc 和 gccgo |
| 1.8 | struct之间的强转;优化并发 map 检测器,支持 map 迭代的并发写 |
| 1.9 | 支持类型别名;新增 sync.Map,math/bits;runtime/pprof 支持添加 label |
| 1.10 | 默认的 GOROOT;优化 gc,降低内存分配的延迟 |
| 1.11 | 新增 go modules;严格化 import path |
| 1.12 | GO111MODULE=on的应用;降低 gc 的分配延迟;更加积极地释放内存 |
| 1.13 | go get 可以更新至 go modules;panic 包含数组下标越界 |
| 1.14 | 允许 interface 嵌套;基于信号的异步抢占式调度器;页面分配器更加高效和更少的锁竞争 |
| 1.15 | 优化链接器的资源使用量,速度以及代码质量;使用更高内核数的系统,分配的小对象要比之前版本快很多 |
| 1.16 | 核心库引入 embed 包,可以将静态文件编译进Go的二进制执行文件中;默认开启 Go modules |
| 1.17 | 改进了编译器,采用了一种新的函数参数和结果传递方式,性能提升了大约 5% |
| 1.18 | 支持泛型;fuzzing(模糊测试)的实现;由于与支持泛型相关的编译器的更改,Go 1.18 的编译速度可能比 Go 1.17 的编译速度慢大约 15% |