这是我参与「第五届青训营」伴学笔记创作活动的第 1 天
0. 内容概述
- 高质量编程
- Go 语言性能优化技巧
- Go 程序性能调优工具 pprof
- 了解工程中性能优化的原则和流程
1. 高质量编程
1.1 高质量编程简介
高质量代码:编写的代码能够达到正确可靠、简洁清晰的目标可称之为高质量代码
- 各种边界条件考虑完备
- 异常情况处理,稳定性保证
- 易读易维护
编程原则:
- 简单性:以简单清晰的逻辑编写代码,消除多余的复杂性
- 可读性:代码是写给人看的,可维护代码的第一步是确保代码可读
- 生产力:团队整体工作效率非常重要
1.2 编码规范
1.2.1 格式化
gofmt:Go 语言官方提供的工具,能自动格式化 Go 语言代码为官方统一风格
goimports: 依赖包管理和字母序排序
1.2.2 注释
注释应该做的:
- 解释代码作用(注释公共符号)
- 解释代码如何做的(注释实现过程)
- 解释代码实现的原因(注释外部因素、额外上下文)
- 解释代码什么时候会出错(解释代码的限制条件)
代码是最好的注释,注释提供代码未表达出的上下文信息
1.2.3 命名规范
- 变量命名
- 简洁胜于冗长
- 缩略词全大写,位于开头且不需要导出时全小写
- 距离被使用地方越远,则需要携带越多的上下文信息
- 函数命名
- 不需要携带 package 的上下文信息,因为函数名和 package 总是成对出现
- 函数名尽量简短
- 当名为 foo 的包内某个函数返回类型 Foo 时,可以省略类型信息
- 当名为 foo 的包内某个函数返回类型不是 Foo 时,可以在函数名中加入类型信息
- 包命名
- 只用小写字母组成,不用大写字母和下划线
- 简短且包含一定上下文信息
- 不要与标准库同名
- 尽量不使用常用变量名
- 尽量使用单数
- 谨慎使用缩写
1.2.4 控制流程
- 减少嵌套,保持正常流程清晰。如
if中包含return,则可以去除冗余的else - 尽量保持正常代码路径为最小缩进。优先处理错误情况,减少分支嵌套
- 故障问题大多出现在复杂的条件语句和循环语句中
1.2.5 错误和异常处理
- 简单错误
- 指仅出现一次的错误,其他地方不需要捕获该错误
- 优先使用
errors.New创建匿名变量来表示简单的错误 - 格式化需求可使用
fmt.Errorf
- 错误的 Wrap 和 Unwrap
- 提供一个 error 嵌套另一个 error 的能力,生成一个 error 的跟踪链
- 在
fmt.Errorf中使用%w关键字来将一个错误关联到错误链中 errors.Is判断一个错误是否在错误链上含有特定的错误errors.As在错误链上获取特定种类的错误
- panic
- 不建议在业务代码中使用
panic - 调用函数不包含
recover会造成程序崩溃 - 程序启动阶段发生不可逆转的错误时,可以在
init或main函数中使用panic - 若问题可以被屏蔽或解决,使用
error即可
- 不建议在业务代码中使用
- recover
- 只能在
defer函数中使用 - 嵌套无法生效
- 只在当前 goroutine 生效
- 如需要更多的上下文信息,可以在
recover后在 log 中记录当前的调用栈
- 只能在
1.3 性能优化建议
1.3.1 Benchmark
- 性能表现需要实际数据衡量
- Go 提供了支持基准性能测试的 Benchmark 工具
1.3.2 Slice
- 使用
make()初始化时,尽可能提供容量信息,预分配内存 - 大切片新建小切片,使用
copy()替代 re-slice 可以使得大切片的底层数组被释放;直接创建切片由于底层数组有引用,得不到释放
1.3.3 Map
- 使用
make()初始化时,尽可能提供容量信息,预分配内存 - 不断向 map 中添加元素会触发扩容
1.3.4 字符串
- 性能:strings.Builder 优于 bytes.buffer 优于直接相加
- 使用
+每次都会重新分配内存 - strings.Builder,bytes.buffer 底层都是
[]byte数组 Grow()方法在字符串长度已知情况下,预分配内存
1.3.5 空结构体
struct{}实例不占用任何内存空间- 可以
map[int]struct{}替代map[int]bool
1.3.6 atomic
- 锁的实现是操作系统实现,成本高
- atomic 操作是硬件实现,效率高
sync.Mutex是用来保护一段逻辑,不仅仅用来保护变量- 对于非数值操作,可以用
atomic.Value,能承载一个interface{}
2 性能调优
2.1 性能调优简介
性能调优原则:
- 依靠数据而不能依靠猜测
- 定位最大瓶颈而不是细枝末节
- 不要过早优化
- 不要过度优化
2.2 性能分析工具 pprof
2.2.1 功能简介
- 分析
- 网页
- 可视化终端
- 展示
- Top
- 调用图-Graph
- 火焰图-FlameGraph
- Peek
- 源码-Source
- 反汇编-Disassemble
- 工具
- runtime/pprof
- net/http/pprof
- 采样
- CPU
- 堆内存-Heap
- 协程-Goroutine
- 锁-Mutex
- 阻塞-Block
- 线程创建-ThreadCreate
2.2.2 示例
- CPU 分析
// 终端启动
go tool pprof "http://localhost:6060/debug/pprof/profile?second=10"
// 查看 CPU 占比 TopN
top
// 定位 CPU 占比代码行,正则匹配过滤
list /exp
// 调用关系可视化
web
- Heap-堆内存
// web 启动,参数 http 表示打开可视化 web 界面
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
- 火焰图
- 由上到下代表调用顺序
- 每一个块代表一个函数,越长代表占用 CPU 时间越长
- 火焰图是动态的,支持点击块进行分析
2.2.3 采样过程和原理
-
CPU
- 采样对象:函数调用及其占用时间
- 采样率:100次每秒,固定值
- 采样时间:手动启动到手动结束
- 操作系统每 10 ms 向进程发送一次 SIGPROF 信号,进程每接收到 SIGPROF 信号会记录到调用堆栈,写缓冲每 100 ms 读取已经记录的调用栈并写入输出流
-
Heap 堆内存
- 通过内存分配器在堆上分配和释放的内存,记录分配/释放的大小和数量
- 采样率:每 512 KB 记录一次,可在运行开头修改
- 采样时间:从程序运行开始到采样时
- 采样指标:alloc_space、alloc_objects、inuse_space、inuse_objects
- 计算方式:inuse=alloc-free
-
Goroutine 协程 & ThreadCreate 线程创建
- Goroutine:记录用户发起的运行中的所有 goroutine(入口非 runtime 开头的)runtime.main 调用栈信息
- ThreadCreate:记录程序创建的所有系统线程信息
-
Block 阻塞 & Mutex 锁
- 阻塞操作
- 采样阻塞操作的次数和耗时
- 采样率:阻塞耗时超过阈值才会被记录,1 为每次阻塞均记录
- 锁竞争
- 采样争抢锁的次数和耗时
- 采样率:只记录固定比例的锁操作,1为每次加锁均记录
- 阻塞操作
总结
本节课主要从编码规范和性能优化建议两个方面讲述了 Go 语言如何实现高质量编程,同时对于 pprof 性能分析工具的用法和原理进行了说明。