这是我参与「第五届青训营 」笔记创作活动的第3天
一、本堂课重点内容
高质量编程
- 高质量编程简介
- 编码规范
- 性能优化建议
性能调优实战
- 性能调优简介
- 性能分析工具pprof实战
- 性能调优案例
二、详细知识点介绍
高质量编程
简介
高质量的概念其实偏主观,一般来说,满足以下四点的可认为是高质量代码:
- 正确:考虑各种边界条件,错误调用处理
- 可靠:异常或错误的处理是否明确,依赖的服务出现异常能否处理
- 简洁:逻辑是否简单,后续功能调整或新增能否快速支持
- 清晰:其他人在阅读时能否清楚明白,重构或修改是否会出现无法预料的问题
编码原则
简单性
- 消除“多余的复杂性”,以简单清晰的逻辑编写代码
- 不理解的代码无法修复改进
可读性
- 代码是写给人看的,而不是机器
- 编写可维护的代码的第一步是确保代码可读
生产力
- 团队整体工作效率非常重要
——Go语言开发者 Dave Cheney
编码规范
对于编码规范,Google和大规模采用Go的公司都有开源的编码规范文档。
下文将从以下五个部分进行介绍:
- 代码格式
- 注释
- 命名规范
- 控制流程
- 错误和异常处理
代码格式
Go 语言官方提供了两种用于格式化的工具:
- gofmt
可以自动格式化代码为官方统一风格,常见的IDE都支持配置
- goimports
可以格式化以外还可以提供依赖包的管理,自动增删依赖的包引用、将依赖包按字母序排序并分类
注释
Good code has lots of comments, bad code requires lots of comments.
好的代码有很多注释,坏代码需要很多注释。
——Dave Thomas and Andrew Hunt
注释应该起到以下作用:
- 解释代码作用
对于对外提供的函数,应该使用注释描述功能和用途,另外对于一些功能简单明显的省略注释。
- 解释代码如何做
对于代码中逻辑复杂的,逻辑不明显的应该使用注释来进行说明实现过程。
- 解释代码实现的原因
对于脱离上下文的代码,通常很难理解,因此应该使用注释来解释为什么这么做。
- 解释代码什么情况下会出错
对于一些条件有限制的代码,应该使用注释来解释当输入非法时,该怎么处理。
另外,对于公共符号始终要注释,这里的公共符号包括变量、常量、函数以及结构体,注意实现接口的方法除外。
最后要牢记两点:
- 代码是最好的注释
- 注释应该提供代码未表达出来的信息
命名规范
- 简洁胜于冗长
- 缩略词全大写,除非位于开头且无需导出时,全小写
- 变量距离被使用的地方越远,则需要携带越多的上下文信息
命名规范的核心目标是降低阅读理解代码的成本,应该重点考虑上下文信息,设计简洁清晰的名称。
Good naming is like a good joke. If you have to explain it, it's not funny.
好的命名就像一个好笑话。如果必须解释它,那就不好笑了。
——Dave Cheney
控制流程
- 避免嵌套,保持正常流程清晰
// Bad code
if foo {
return x
} else {
return nil
}
// Nice code
if foo {
return x
}
return nil
- 尽量保持正常代码路径为最小缩进
优先处理错误,尽早返回。
// Bad code
func OneFunc() error {
err := doSomething()
if err == nil {
err := doAnotherThing()
if err == nil {
return nil
}
return err
}
}
// Nice code
func OneFunc() error {
if err := doSomething(); err != nil {
return err
}
if err := doAnotherThing(); err != nil {
return err
}
return nil
}
小结
- 线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支
- 正常流程代码沿着屏幕向下移动
- 提升代码可维护性和可读性
- 故障问题大多出现在复杂的条件语句和循环语句中
错误和异常处理
对于简单错误(仅出现一次的错误,且在其他地方不需要捕获该错误)
- 优先使用
errors.New来创建匿名变量来直接表示简单错误 - 如有格式化需求,使用
fmt.Errorf
错误的Wrap和Unwrap
- 错误的Wrap实际上是提供一个error嵌套另一个error的能力,从而生成一个error的跟踪链
- 在
fmt.Errorf中使用%w关键字来将一个错误关联至错误链中
错误判定
- 判断一个错误是否为特定错误使用
errors.Is - 不同于
==,使用该方法可以判断错误链上是否含有该错误 - 在错误链上获取特定种类的错误,使用
errors.As
panic
- 不建议业务中使用
- 调用函数不包含
recover会造成程序崩溃 - 若问题可以被屏蔽或解决,建议使用
error代替panic - 当程序启动阶段发生不可逆转的错误时,可以在
init或main函数中使用panic
recover
- 只能在
defer中使用 - 嵌套不生效
- 只对当前goroutine有效
defer后入先出
小结
- error尽可能提供简明的上下文信息链,方便定位问题
- panic用于真正异常的情况
- recover生效范围,在当前goroutine的被defer函数中生效
性能优化建议
测试工具
- benchmark
使用方法go test -bench=. -benchmem
以下是优化建议:
- slice预分配内存(减少扩容次数)
- 在原切片基础上切片时,新切片会占用原始切片的底层数组,导致无法释放引用,使用
make先新建切片,copy来将旧切片切片拷贝到新的切片上,这样就不会引用原来的底层数组,也就可以让底层数组释放。 - map预分配内存(减少Rehash次数)
- 使用
strings.Builder来拼接字符串,对于拼接操作,bytes.Buffer与strings.Builder相近,使用+最慢。因为在Go语言中,字符串是不可变类型,占用的内存大小是固定的,每次使用+都会重新分配内存。而前两者底层都是[]byte,因此不需要每次拼接都重新分配内存(扩容除外)。而strings.Builder更快的原因是因为其直接将底层[]byte转为字符串类型返回(先转为*string,然后再使用*将其变为string类型),而bytes.Buffer转为字符串时申请了一块空间。 - 使用空结构体节省内存。因为空结构体
struct{}实例不占据任何的内存空间,因此也可以作为占位符使用。实现Set时,可以使用map+空结构体。 - 使用atomic包来保证计数器的准确。与Mutex相比:
| Mutex | atomic | |
|---|---|---|
| 实现方法 | 操作系统 | 硬件 |
| 保护对象 | 一段逻辑 | 一个变量 |
| 特殊 | / | 非数值使用atomic.Value |
性能调优实战
性能调优简介
性能调优原则
- 要依靠数据而不是猜测
- 要定位最大瓶颈而不是细枝末节
- 不要过早优化
- 不要过度优化
性能分析工具pprof实战
pprof是用于可视化、分析性能和分析数据的工具。
接下来介绍pprof采样的过程和原理
1. CPU
graph LR
开始采样 --> 设定信号处理函数
设定信号处理函数 --> 开启定时器
停止采样 --> 取消信号处理函数
取消信号处理函数 --> 关闭定时器
对于CPU采样,其会记录所有的调用栈和它们的占用时间。在采样时,进程每秒暂停100次,每次会记录当前的调用栈信息。汇总之后,根据调用栈在采样中出现的次数来推断运行的时间,其依赖信号实现。
启动采样时,进程向OS注册一个定时器,OS每隔10ms向进程发送一个SIGPROF信号,进程收到后就对当前调用栈进行记录。与此同时,进程同时会启动一个写缓存的goroutine,它每隔100ms从进程中读取已经记录的堆栈信息,并写入到输出流,当停止采样时,进程向OS取消定时器,不再接收信号,写缓冲读取不到新堆栈信息时就结束输出。
2. Heap内存
之所以写的是“Heap内存”而不是“内存”,是因为内存采样时是依赖内存分配器的记录,所以只能记录堆上分配,且会参与GC的内存,一些其他的内存分配,例如调用就结束就会回收的栈内存、一些更底层使用cgo调用分配的内存,是不会被内存采样记录的。其采样率默认512KB采样一次,其为一个持续的过程,记录从程序运行起的所有分配或释放的内存大小和对象数量,并在采样时汇总。
3. Goroutine
graph LR
Stop-The-World --> 遍历allg切片
遍历allg切片 --> 输出创建g的堆栈
输出创建g的堆栈 --> Start-The-World
也就是在使用“咋瓦鲁多”后,遍历所有goroutine的列表并输出堆栈,最后恢复。注意这个采样是立刻触发的,记录所有goroutine的。
4. Block
graph TD
阻塞操作 --> |上报调用栈和消耗时间|Profiler
Profiler --> |采样|遍历阻塞记录
遍历阻塞记录 --> 统计阻塞次数和耗时
Profiler --> 时间未到阈值则丢弃
该采样只有当达到一个阈值时才会被记录,也就是阻塞时间达到一定时间就会进行采样,采样的内容是当时对应操作发生的调用者、次数和耗时。
三、实践练习例子
具体实践可以参考此文章golang pprof实战。
四、课后个人总结
通过此次课程,我学会了不少有关Golang的高质量编程方法与性能调优工具以及技巧,对于高质量编程,最为重要的就是保证正确、可靠、简洁、清晰。性能优化方面学习到了不少提高性能的方法,编写程序不是写好逻辑就算完成,写出高性能的代码是非常重要的。对于性能调优,pprof工具真的是非常强大,不仅能够控制台调试,还能够提供ui界面供查看。总的来说,这次课程收获不小。