高质量编程与性能调优实战丨青训营笔记

132 阅读9分钟

这是我参与「第五届青训营」伴学笔记创作活动的第16天

1 高质量编程

1.1 简介

高质量:编写的代码能够达到正确可靠、简洁清晰的目标可称之为高质量代码。

  • 各种边界条件是否考虑完备
  • 异常情况处理,稳定性保证
  • 易读易维护

编程原则

  • 简单性
    • 逻辑简单清晰,消除多余的复杂性
    • 避免难以理解的代码(无法修复改进)
  • 可读性
    • 写给人看的(易懂可读)
    • 确保代码可读,才能保证代码的可维护
  • 生产力
    • 要保证团队的整体工作效率

1.2 编码规范

如何编写高质量的Go代码?

1.2.1 代码格式

推荐使用gofmt自动格式化代码,使用goimports实现依赖包管理、自动增删依赖的包引用、将依赖包按字母排序并分类。

1.2.2 注释

  1. 解释代码作用

  2. 解释代码如何做的

  3. 解释代码实现的原因

    • 解释代码的外部因素
    • 提供额外上下文
  4. 解释代码在什么情况会出错

    • 解释代码的限制条件

1.2.3 命名规范

  1. 变量命名
    • 简洁胜于冗长
    • 缩略词全大写,但当其位于开头且不需要导出时,使用全小写
      • 例如ServeHTTP而不是ServeHttp
      • 使用XMLHTTPRequest或者xmlHTTPRequest
    • 变量距离其被使用的地方越远,则需要携带越多的上下文信息
      • 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认其含义

image.png

iindex的作用域范围仅限于for循环内部时,index的额外冗长几乎没有增加对于程序的理解,应从简。

image.png

将`deadline`替换成`t`降低了变量名的信息量,`t`通常代指时间,`deadline`指截止时间,有特定含义,`deadline`更优。
  1. 函数命名
    • 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现
    • 函数名尽量简短
    • 当名为foo的包的某个函数返回类型Foo时,可以省略类型信息以免歧义
    • 当名为foo的包某个函数返回类型T时(T不为Foo),可以在函数名中加入类型信息

image.png

在包下调用函数,函数名可不携带包名信息,如:http.Serve()http.ServeHTTP()更简洁。

  1. package命名
  • 只由小写字母组成,不包含大写字母或下划线
  • 简短并包含一定的上下文信息。如schematask
  • 不要与标准库重名。例如不要使用syncstrings命名

尽量满足:

  • 不使用常用变量名作为包名。例如使用bufio而不是buf
  • 使用单数命名。例如使用encoding而不是encodings
  • 合理谨慎地使用缩写。如fmt在不破坏上下文的情况下,比format更简洁

1.2.4 控制流程

  1. 避免嵌套,保持正常流程清晰。

image.png

如果两个分支都包含return语句,则可以去除冗余的else

  1. 尽量保持代码路径为最小缩进
  • 优先处理错误/特殊情况

image.png

小结

  • 线性原理,处理逻辑尽量走直线,避免复杂嵌套分支
  • 正常流程代码沿着屏幕向下移动
  • 提升代码的可维护性和可读性
  • 故障问题大多出现在复杂的条件语句和循环语句中

1.2.5 错误和异常处理

简单错误

  • 仅出现一次的错误,且在其他地方不需要捕获该错误。
  • 优先使用errors.New()来创建匿名变量来表示简单错误
  • 如果有格式化的需求,使用fmt.Errorf()

复杂错误

  1. 错误的包装(Wrap)与解包(Unwrap)
  • 提供了一个error嵌套另一个error的跟踪链,使每一层的调用方都可以补充上下文信息
  • fmt.Errorf()中使用%w关键字来将一个错误关联至错误链中。

image.png

  1. 错误判定
  • 判定是否为特定错误,使用error.Is()
  • 不同于使用==,使用该方法可以判定错误链上的所有错误是否包含有特定的错误

image.png

  • 在错误链上获取特定种类的错误,使用error.As()

image.png

  1. panic
  • 不建议在业务代码中使用panic
  • 调用函数不包含recover会造成程序崩溃
  • 若问题可以被屏蔽或解决,建议使用error代替panic
  • 当程序启动阶段出现不可逆转的错误时,可以在initmain函数中使用panic(尽早暴露错误)
  1. recover
  • recover只能在被defer的函数中使用
  • 嵌套无法生效
  • 只能在当前goroutine生效
  • defer的语句是后进先出
  • 如果需要更多的上下文信息,可以recover后在log中记录当前的调用栈

小结

  • error尽可能提供简明的上下文信息链,方便定位问题
  • painc用于真正异常的情况
  • recover在当前goroutine的被defer的函数中生效

1.3 性能优化建议

1.3.1 Benchmark

Go语言提供了支持基准性能测试的benchmark工具。

image.png

go test -bench=. -benchmem

image.png

1.3.2 Slice

  1. slice预分配内存
  • 尽可能在使用make()初始化切片时指定容量信息

image.png

原理:

image.png

  1. 大内存未释放
  • 在已有切片基础上创建切片,不会创建新的底层数组
  • 场景
    • 原切片较大,代码在原切片基础上新建小切片
    • 原底层数组在内存中有引用,得不到释放
  • 可使用copy代替re-slice

image.png

使用go test -run=. -v测试

image.png

1.3.3 Map

  1. map内存预分配

1.3.4 字符串处理

  1. 使用strings.Builder

常见的字符串拼接方式

  • 相加拼接
  • strings.Builder
  • byte.Buffer

image.png

image.png

image.png

性能比较: image.png

image.png

1.3.5 空结构体

  1. 使用空结构体节省内存
image.png

1.3.6 使用atomic包

使用atomic包代替加锁,保证线程安全同时性能更好。

image.png

2. 性能调优实战

2.1 简介

性能调优原则

  • 要依靠数据而非猜测
  • 要定位最大瓶颈而非细枝末节
  • 不要过早优化
  • 不要过度优化

2.2 性能分析工具pprof

说明

  • pprof是用于可视化和分析性能数据的工具
  • 知道应用在什么地方耗费了多少CPU、Memory

2.2.1 pprof性能简介

image.png

2.2.2 pprof排查实战

下文将通过一个小项目的实战,来进一步熟悉pprof的各种使用方法。

克隆如下项目,并用VS Code打开运行:

git clone https://github.com/wolfogre/go-pprof-practice.git
go run main.go

终端输出:

image.png

通过网页可访问:

http://localhost:6060/debug/pprof/

image.png

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关键字,查看资源占用最多的函数,其中一些相关指标如下:

image.png

其中,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

输出调用关系图如下:

pprof001.svg

尝试注释相关高占用资源的代码,比较优化前后的运行占用情况以及运行耗时。

注释前:

image.png

注释后:(CPU明显降低,但内存问题仍未解决)

image.png

(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/

image.png

image.png

分析可知,mouse.Steal方法最占内存,注释掉相关代码,重新运行,查看结果:

image.png

占用内存已大幅下降。

image.png

2.2.2.3 goroutine协程分析

goroutine泄露也会导致内存泄露,故需进行协程分析。

go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"

常用火焰图Flame Graph来分析,其特点是:

  • 从上到下表示调用顺序
  • 每一块代表一个函数,越长代表占用CPU的时间更长
  • 火焰图是动态的,支持点击块进行分析

image.png

image.png

将其注释掉后再观察结果,协程数大幅下降:

image.png

image.png

2.2.2.4 锁阻塞分析

go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"

image.png

image.png

image.png

再次运行,比较注释前后数据:

image.png image.png

2.2.2.5 block阻塞分析

go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"
image.png image.png

重新运行即可。

2.2.2.6 小结

image.png

2.2.3 采样过程和原理

2.2.3.1 CPU采样

  • 采样对象:函数调用和它们占用的时间
  • 采样率:100次/s,固定值
  • 采样时间:从手动启动到手动结束

image.png

image.png

2.2.3.2 堆内存采样

image.png

2.2.3.3 Goroutine协程 & ThreadCreate线程创建

image.png

2.2.3.4 Block阻塞 & Mutex锁

image.png

2.3 性能调优案例

介绍实际业务服务性能优化的案例,对逻辑相对复杂的当程序如何进行性能调优。

  • 业务服务优化
  • 基础库优化
  • Go语言优化

2.3.1 业务服务优化

基本概念

  • 服务:能单独部署,承载一定功能的程序
  • 依赖:若Service A的功能实现依赖Service B的响应结果,则称为Service A依赖Service B
  • 调用链路:能支持一个接口请求的相关服务集合及其相互之间的依赖关系
  • 基础库:公共的工具包、中间件

image.png

流程

  • 建立服务性能评估手段
  • 分析性能数据,定位性能瓶颈
  • 重点优化项改造
  • 优化效果验证

image.png

image.png