这是我参与「第五届青训营」伴学笔记创作活动的第16天
1 高质量编程
1.1 简介
高质量:编写的代码能够达到正确可靠、简洁清晰的目标可称之为高质量代码。
- 各种边界条件是否考虑完备
- 异常情况处理,稳定性保证
- 易读易维护
编程原则
- 简单性
- 逻辑简单清晰,消除多余的复杂性
- 避免难以理解的代码(无法修复改进)
- 可读性
- 写给人看的(易懂可读)
- 确保代码可读,才能保证代码的可维护
- 生产力
- 要保证团队的整体工作效率
1.2 编码规范
如何编写高质量的Go代码?
1.2.1 代码格式
推荐使用gofmt自动格式化代码,使用goimports实现依赖包管理、自动增删依赖的包引用、将依赖包按字母排序并分类。
1.2.2 注释
-
解释代码作用
-
解释代码如何做的
-
解释代码实现的原因
- 解释代码的外部因素
- 提供额外上下文
-
解释代码在什么情况会出错
- 解释代码的限制条件
1.2.3 命名规范
- 变量命名
- 简洁胜于冗长
- 缩略词全大写,但当其位于开头且不需要导出时,使用全小写
- 例如
ServeHTTP而不是ServeHttp - 使用
XMLHTTPRequest或者xmlHTTPRequest
- 例如
- 变量距离其被使用的地方越远,则需要携带越多的上下文信息
- 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认其含义
i和index的作用域范围仅限于for循环内部时,index的额外冗长几乎没有增加对于程序的理解,应从简。
- 函数命名
- 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现
- 函数名尽量简短
- 当名为foo的包的某个函数返回类型Foo时,可以省略类型信息以免歧义
- 当名为foo的包某个函数返回类型T时(T不为Foo),可以在函数名中加入类型信息
在包下调用函数,函数名可不携带包名信息,如:http.Serve()比http.ServeHTTP()更简洁。
- package命名
- 只由小写字母组成,不包含大写字母或下划线
- 简短并包含一定的上下文信息。如
schema、task等 - 不要与标准库重名。例如不要使用
sync或strings命名
尽量满足:
- 不使用常用变量名作为包名。例如使用
bufio而不是buf - 使用单数命名。例如使用
encoding而不是encodings - 合理谨慎地使用缩写。如
fmt在不破坏上下文的情况下,比format更简洁
1.2.4 控制流程
- 避免嵌套,保持正常流程清晰。
如果两个分支都包含return语句,则可以去除冗余的else。
- 尽量保持代码路径为最小缩进
- 优先处理错误/特殊情况
小结:
- 线性原理,处理逻辑尽量走直线,避免复杂嵌套分支
- 正常流程代码沿着屏幕向下移动
- 提升代码的可维护性和可读性
- 故障问题大多出现在复杂的条件语句和循环语句中
1.2.5 错误和异常处理
简单错误
- 仅出现一次的错误,且在其他地方不需要捕获该错误。
- 优先使用
errors.New()来创建匿名变量来表示简单错误 - 如果有格式化的需求,使用
fmt.Errorf()
复杂错误:
- 错误的包装(Wrap)与解包(Unwrap)
- 提供了一个error嵌套另一个error的跟踪链,使每一层的调用方都可以补充上下文信息
- 在
fmt.Errorf()中使用%w关键字来将一个错误关联至错误链中。
- 错误判定
- 判定是否为特定错误,使用
error.Is() - 不同于使用
==,使用该方法可以判定错误链上的所有错误是否包含有特定的错误
- 在错误链上获取特定种类的错误,使用
error.As()
- panic
- 不建议在业务代码中使用
panic - 调用函数不包含
recover会造成程序崩溃 - 若问题可以被屏蔽或解决,建议使用
error代替panic - 当程序启动阶段出现不可逆转的错误时,可以在
init或main函数中使用panic(尽早暴露错误)
- recover
recover只能在被defer的函数中使用- 嵌套无法生效
- 只能在当前goroutine生效
defer的语句是后进先出- 如果需要更多的上下文信息,可以
recover后在log中记录当前的调用栈
小结:
error尽可能提供简明的上下文信息链,方便定位问题painc用于真正异常的情况recover在当前goroutine的被defer的函数中生效
1.3 性能优化建议
1.3.1 Benchmark
Go语言提供了支持基准性能测试的benchmark工具。
go test -bench=. -benchmem
1.3.2 Slice
- slice预分配内存
-
尽可能在使用
make()初始化切片时指定容量信息
原理:
- 大内存未释放
- 在已有切片基础上创建切片,不会创建新的底层数组
- 场景
- 原切片较大,代码在原切片基础上新建小切片
- 原底层数组在内存中有引用,得不到释放
- 可使用
copy代替re-slice
使用go test -run=. -v测试
1.3.3 Map
- map内存预分配
1.3.4 字符串处理
- 使用strings.Builder
常见的字符串拼接方式
- 相加拼接
- strings.Builder
- byte.Buffer
性能比较:
1.3.5 空结构体
- 使用空结构体节省内存
1.3.6 使用atomic包
使用atomic包代替加锁,保证线程安全同时性能更好。
2. 性能调优实战
2.1 简介
性能调优原则
- 要依靠数据而非猜测
- 要定位最大瓶颈而非细枝末节
- 不要过早优化
- 不要过度优化
2.2 性能分析工具pprof
说明
- pprof是用于可视化和分析性能数据的工具
- 知道应用在什么地方耗费了多少CPU、Memory
2.2.1 pprof性能简介
2.2.2 pprof排查实战
下文将通过一个小项目的实战,来进一步熟悉pprof的各种使用方法。
克隆如下项目,并用VS Code打开运行:
git clone https://github.com/wolfogre/go-pprof-practice.git
go run main.go
终端输出:
通过网页可访问:
http://localhost:6060/debug/pprof/
2.2.2.1 CPU资源占用分析
新建终端运行:
>>go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
Fetching profile over HTTP from http://localhost:6060/debug/pprof/profile?seconds=10
Saved profile in C:\Users\青fa\pprof\pprof.samples.cpu.002.pb.gz
Type: cpu
Time: Feb 20, 2023 at 4:54pm (CST)
Duration: 10.01s, Total samples = 3.17s (31.68%)
Entering interactive mode (type "help" for commands, "o" for options)
进入pprof模式后,输入top关键字,查看资源占用最多的函数,其中一些相关指标如下:
其中,flat与当前函数本身有关;cum与当前函数加上其调用函数有关。
那么,当flat==cum,则函数中没有调用其他函数;当flat=0,函数只有关于其他函数的调用。
>>(pprof)top //查看资源占用最多的函数
Showing nodes accounting for 3.15s, 99.37% of 3.17s total
Dropped 16 nodes (cum <= 0.02s)
Showing top 10 nodes out of 15
flat flat% sum% cum cum%
3.12s 98.42% 98.42% 3.12s 98.42% github.com/wolfogre/go-pprof-practice/animal/felidae/tiger.(*Tiger).Eat
0.02s 0.63% 99.05% 0.02s 0.63% runtime.cgocall
0.01s 0.32% 99.37% 0.02s 0.63% github.com/wolfogre/go-pprof-practice/animal/felidae/cat.(*Cat).Live
0 0% 99.37% 3.12s 98.42% github.com/wolfogre/go-pprof-practice/animal/felidae/tiger.(*Tiger).Live
0 0% 99.37% 0.02s 0.63% internal/poll.(*FD).Write
0 0% 99.37% 0.02s 0.63% internal/poll.(*FD).writeConsole
0 0% 99.37% 0.03s 0.95% log.(*Logger).Output
0 0% 99.37% 0.03s 0.95% log.Println
0 0% 99.37% 3.16s 99.68% main.main
0 0% 99.37% 0.02s 0.63% os.(*File).Write
使用list关键字,可以根据指定的正则表达式查找代码行。那么,我们已经从上文的top关键字得到占用资源最多的几个函数关键字,不妨使用list方法查找一下,如list Eat,结果如下:
>>(pprof) list Eat
Total: 3.17s
ROUTINE ======================== github.com/wolfogre/go-pprof-practice/animal/canidae/dog.(*Dog).Eat in C:\Users\青fa\Desktop\GoCourse_ByteTech\A_Go语言原理与实践\04_go-pprof-practice\animal\canidae\dog\dog.go
0 10ms (flat, cum) 0.32% of Total
. . 21: d.Run()
. . 22: d.Howl()
. . 23:}
. . 24:
. . 25:func (d *Dog) Eat() {
. 10ms 26: log.Println(d.Name(), "eat")
. . 27:}
. . 28:
. . 29:func (d *Dog) Drink() {
. . 30: log.Println(d.Name(), "drink")
. . 31:}
ROUTINE ======================== github.com/wolfogre/go-pprof-practice/animal/felidae/tiger.(*Tiger).Eat in C:\Users\青fa\Desktop\GoCourse_ByteTech\A_Go语言原理与实践\04_go-pprof-practice\animal\felidae\tiger\tiger.go
3.12s 3.12s (flat, cum) 98.42% of Total
. . 19:}
. . 20:
. . 21:func (t *Tiger) Eat() {
. . 22: log.Println(t.Name(), "eat")
. . 23: loop := 10000000000
3.12s 3.12s 24: for i := 0; i < loop; i++ {
. . 25: // do nothing
. . 26: }
. . 27:}
. . 28:
. . 29:func (t *Tiger) Drink() {
使用web关键字,可使调用关系可视化。
注意,如果报错:
Failed to execute dot. Is Graphviz installed? Error: exec: "dot": executable file not found in %PATH%请到www.graphviz.org/download/下载
Graphviz, 并配置到环境变量中,将bin目录配置到PATH。
>>(pprof) web
输出调用关系图如下:
尝试注释相关高占用资源的代码,比较优化前后的运行占用情况以及运行耗时。
注释前:
注释后:(CPU明显降低,但内存问题仍未解决)
(pprof) top
Showing nodes accounting for 300ms, 73.17% of 410ms total
Showing top 10 nodes out of 75
flat flat% sum% cum cum%
180ms 43.90% 43.90% 180ms 43.90% runtime.stdcall3
20ms 4.88% 48.78% 20ms 4.88% runtime.deltimer
20ms 4.88% 53.66% 80ms 19.51% runtime.findRunnable
20ms 4.88% 58.54% 20ms 4.88% runtime.siftdownTimer
10ms 2.44% 60.98% 10ms 2.44% fmt.Sprintln
10ms 2.44% 63.41% 20ms 4.88% log.Println
10ms 2.44% 65.85% 10ms 2.44% runtime.(*mcache).prepareForSweep
10ms 2.44% 68.29% 10ms 2.44% runtime.(*pageAlloc).update
10ms 2.44% 70.73% 10ms 2.44% runtime.(*pageBits).popcntRange
10ms 2.44% 73.17% 10ms 2.44% runtime.(*scavengeIndex).find
2.2.2.2 内存占用分析
上文解决了CPU资源占用的问题,接下来我们通过pprof工具管理Heap堆内存来解决内存占用问题。
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
对应打开网页:
http://localhost:8080/ui/
分析可知,mouse.Steal方法最占内存,注释掉相关代码,重新运行,查看结果:
占用内存已大幅下降。
2.2.2.3 goroutine协程分析
goroutine泄露也会导致内存泄露,故需进行协程分析。
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
常用火焰图Flame Graph来分析,其特点是:
- 从上到下表示调用顺序
- 每一块代表一个函数,越长代表占用CPU的时间更长
- 火焰图是动态的,支持点击块进行分析
将其注释掉后再观察结果,协程数大幅下降:
2.2.2.4 锁阻塞分析
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"
再次运行,比较注释前后数据:
2.2.2.5 block阻塞分析
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"
重新运行即可。
2.2.2.6 小结
2.2.3 采样过程和原理
2.2.3.1 CPU采样
- 采样对象:函数调用和它们占用的时间
- 采样率:100次/s,固定值
- 采样时间:从手动启动到手动结束
2.2.3.2 堆内存采样
2.2.3.3 Goroutine协程 & ThreadCreate线程创建
2.2.3.4 Block阻塞 & Mutex锁
2.3 性能调优案例
介绍实际业务服务性能优化的案例,对逻辑相对复杂的当程序如何进行性能调优。
- 业务服务优化
- 基础库优化
- Go语言优化
2.3.1 业务服务优化
基本概念
- 服务:能单独部署,承载一定功能的程序
- 依赖:若
Service A的功能实现依赖Service B的响应结果,则称为Service A依赖Service B - 调用链路:能支持一个接口请求的相关服务集合及其相互之间的依赖关系
- 基础库:公共的工具包、中间件
流程
- 建立服务性能评估手段
- 分析性能数据,定位性能瓶颈
- 重点优化项改造
- 优化效果验证