高质量编程 | 青训营笔记

45 阅读17分钟

本节课的目标是学习:

  • 如何编写更常用简洁清晰的代码
  • 常用Go语言程序优化手段
  • 熟悉Go程序性能分析工具
  • 了解工程中性能优化的原则和流程

1.高质量编程

1.1 简介

什么是高质量

---编写的代码能够达到正确可靠、简洁清晰

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

编程原则

实际应用场景千变万化,各种语言的特性和语法各不相同,但是高质量编程遵循的原则是相通的

简单性
  • 消除“多余的复杂性”,以简单清晰的逻辑编写代码
  • 不理解的代码无法修复改进
可读性
  • 代码是写给人看的,而不是机器
  • 编写可维护代码的第一步是确保代码可读
生产力
  • 团队整体工作效率非常重要

1.2编码规范

如何编写高质量Go代码

  • 代码格式
  • 注释
  • 命名规范
  • 控制流程
  • 错误和异常处理

1.2.1代码格式

推荐使用gofmt自动格式化代码

gofmt:

Go语言官方提供的工具,能自动格式化Go语言代码为官方统一风格,常见的IDE都支持方便的配置。

goimports:

也是Go语言官方提供的工具,实际等于gofmt加上依赖包管理,自动增删依赖的包引用、将依赖包按字母序排序并分类等。

1.2.2注释

注释应该做的:

  • 注释应该解释代码的作用,实现的功能
  • 注释应该解释代码如何做的,实现的逻辑
  • 注释应该解释代码实现的原因,描述一下代码的上下文背景
  • 注释应该解释代码什么情况会出错,限制条件

公共符号始终要注释

  • 包中声明的每个公共的符号:变量、常量、函数以及结构都需要添加注释
  • 任何既不明显也不简短的公共功能必须予以注释
  • 无论长度或者复杂程度如何,对库中的任何函数都必须进行注释
  • 有一个例外,不需要注释实现接口的方法。
小结:
  • 代码是最好的注释
  • 注释应该提供代码未表达出的上下文信息

1.2.3 命名规范

变量名
  • 简洁胜于冗长
  • 略缩词全大写,但当其位于变量开头且不需要导出时,使用全小写

如ServerHTTP而不是ServerHttp

如XMLHTTPRequest或者xmlHTTPRequest

  • 变量距离其被使用的地方越远,则需要携带更多的上下文信息

    全局变量在其名字中需要更多的上下文信息,使得在不同的地方可以轻易辨认出其含义

    image-20230117171958005

函数名
  • 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的

  • 函数名尽量简短

  • 当名为foo的包中某个函数返回类型为Foo时,可以省略类型信息而不导致歧义

    下面的示例中方法一命名更好。

  • image-20230117172302908

  • 当名为foo的包中某个函数返回类型T时(T并不是Foo),可以在函数名中加入类型信息

包名
  • 只由小写字母组成,不包含大学字母和下划线等字符
  • 简短并包含一定的上下文信息。如schema、task等
  • 不要与标准库同名。如不可以使用sync或strings包

下面的规则需要尽量满足,以标准库包名为例

  • 不使用常用变量名作为包名,如使用bufio而不使用buf
  • 使用单数而不是复数,如encoding而不是encodings
  • 谨慎使用缩写,如使用fmt在不破坏上下文的情况下比format更加简短
小结
  • 核心目标是降低阅读理解代码的成本
  • 重点考虑上下文信息,设计简洁清晰的名称

1.2.4 控制流程

  • 避免嵌套,保证正常流程清晰,如下:

image-20230117173255140

  • 尽量保证正常代码路径为最小缩进

    优先处理错误情况/特殊情况,尽早返回或继续循环来减少嵌套。

    最常见的正常流程的路径被嵌套在两个if条件内

    成功的退出条件为return nil,必须仔细匹配大括号来发现

    函数最后一行返回一个错误,需要追溯到匹配的左括号,才能了解合适会触发错误

    如果后续正常流程需要增加一步操作,调用新的函数,则又会增加一层嵌套

    image-20230117173918899image-20230117173941567

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

1.2.5 错误和异常处理

简单错误
  • 简单的错误指的是仅出现一次的错误,且在其他地方不需要捕获该错误
  • 优先使用errors.New来创建匿名变量来直接表示简单错误
  • 如果有格式化的需求,使用fmt.Errorf
错误的Wrap和Unwrap
  • 错误的Wrap实际上是提供了一个error嵌套另一个error的能力,从而生成一个error的跟踪链
  • 在fmt.Errorf中使用:%w关键字来将一个错误关联至错误链中
  • image-20230117175205625
错误判定
  • 判定一个错误是否为特定错误,使用errors.ls,不同于使用==,该方法可以判定错误链上的所有错误是否含有特定的错误
  • image-20230117175456683
  • 在错误链上获取特定种类的错误,使用error.As
  • image-20230117175602848
panic
  • 不建议在业务代码中使用panic
  • 调用函数不包含recover会造成程序崩溃
  • 若问题可以被屏蔽或解决,建议使用error代替panic
  • 当程序启动阶段发生不可逆转的错误时,可以在init或main函数中使用painc
  • image-20230117175950093
recover
  • recover只能在被defer的函数中使用
  • 嵌套无法生效
  • 只在当前goroutine生效
  • defer的语句是后进先出
  • image-20230117180810351
  • 如果需要更多的上下文信息,可以recover后在log中记录当前的调用栈。
  • image-20230117180946190
小结
  • error尽可能提供简明的上下文信息链,方便定位问题
  • panic用于真正异常的情况
  • recover生效范围,在当前goroutine的被defer的函数中生效

1.3性能优化建议

1.3.1 简介

image-20230117190854139

1.3.2 基准性能测试Benchmark

image-20230117192146597

下面是测试命令

image-20230117192205381

image-20230117192317493

执行结果展说明:

image-20230117192359749

1.3.3 scile优化

slice预分配内存

尽可能在使用make()初始化切片时提供容量信息。

image-20230117192612096

提前分配容量比未提前分配内存速度快了约1/3左右。

另一个陷阱:大内存未释放

image-20230117193039195

image-20230117193056402

1.3.4 map优化

image-20230117193206237

1.3.5 字符串处理优化

使用strings.Builder

常用的3中字符串拼接的性能对比

image-20230117193507785

image-20230117193545958

image-20230117193602821

可以看到直接用+来进行拼接,性能会非常差,用buffer和strings.Builder性能比较好,但是可以发现strings.Builder性能是最好的。

image-20230117195238289

image-20230117195313432image-20230117195343354

image-20230117195428542

通过上面的示例,我们也可以考虑提前给strings.Builder和buffer提前分配内存,来看看运行效率

image-20230117195814901

1.3.6 空结构体的使用

使用空结构体节省内存

image-20230117195929062image-20230117195953504

image-20230117200035882

image-20230117200049339

1.3.7atomic包

如何使用atomic包

在工作中遇到多线程的场景时,左边的方式使用了atomic包,维护了一个原子的变量,通过对原子变量的操作来保证技术准确,线程安全。右边是通过加锁的方式来实现的,

image-20230117200228648

image-20230117200239526

通过比较发现使用atomic包比加锁的性能要高一些。

image-20230117200647119

1.3.8 性能优化小结

image-20230117200751306

2.性能调优实战

2.1简介

性能调优原则

image-20230117201002336

2.2 性能分析工具 pprof

如果我们希望知道应用在什么地方耗费了多少cpu、Memory,可以使用pprof,这是一款用于可视化和分析性能分析数据的工具。

2.2.1 pprof - 功能简介

image-20230117201235256

2.2.2 pprof - 排查实战

在本地搭建好一个实践项目,项目里面提前埋入了一些炸弹代码,产生可观测的性能问题。

项目地址:github.com/wolfogre/go…

项目实战手册:blog.wolfogre.com/posts/go-pp…

把项目开始运行,项目大约会占用2G的内存以及2核的cpu

项目运行后打开网页:http://localhost:6060/debug/pprof/

页面上展示了可用的程序运行采样数据,分别有:

image-20230117204535559

因为 cmdline 没有什么实验价值,trace 与本文主题关系不大,threadcreate 涉及的情况偏复杂,所以这三个类型的采样信息这里暂且不提。除此之外,其他所有类型的采样信息本文都会涉及到,且炸弹程序已经为每一种类型的采样信息埋藏了一个对应的性能问题。

由于直接阅读采样信息缺乏直观性,我们需要借助 go tool pprof 命令来排查问题,这个命令是 go 原生自带的,所以不用额外安装,我们一边实战一边学习。

排查CPU问题

运行如下命令:go tool pprof http://localhost:6060/debug/pprof/profile

也可以设置采样时间:go tool pprof http://localhost:6060/debug/pprof/profile?seconds=10

会显示下面的界面,进入一个交互式终端

image-20230117205403759

我们再输入top命令,查看 CPU 占用较高的调用:

image-20230117205505337

image-20230117205612120

image-20230117205825145image-20230117205855713

发现eat代码占用率最高,接下来输入 list Eat,可以查看问题具体在代码的哪一个位置:

image-20230117210115746

可以看到第24行的for循环占用了非常大的cpu时间

除此之外还可以在安装了graphviz之后,输入web命令,去网页中可视化的查看数据

graphviz官网:graphviz.gitlab.io/download/

image-20230117212810739

看到框最大的就像想提醒我们,偷内存的贼就是它,我们修复一下这个问题,也就是把相关的代码注释掉。

cpu的问题已经定位到了。

重新编译执行后发现cpu的占用已经下来了,但是内存占用依然很高

Heap堆内存

查堆内存使用命令:go tool pprof http://localhost:6060/debug/pprof/heap

结果如下:

image-20230117215150561

也同样可以用top和list命令来排查

如果希望在浏览器中可视化的排查,可以在命令前面加上一些内容,如下:

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

image-20230117214731587

依然可以看到占用内存最高的最大的红色方框,网页左上角的view中可以展示多种方式的图。

top视图如下:类似于刚刚top命令的视图版

image-20230117215046529

source视图如下:

image-20230117215244387

找到了Steal方法里的第50行代码非常占内存,可以看到,这里有个循环会一直向 m.buffer 里追加长度为 1 MiB 的数组,直到总容量到达 1 GiB 为止,且一直不释放这些内存,这就难怪会有这么高的内存占用了。我们去注释掉。

对了Ctrl+c可以结束命令行正在运行的命令。

此外,频繁的 GC 对 golang 程序性能的影响也是非常严重的。虽然现在这个炸弹程序内存使用量并不高,但这会不会是频繁 GC 之后的假象呢?

对于网页展示图的右边有一个inuse的蓝色字,在左边SAMPLE下拉菜单中可以看到4个采样说明可以选

image-20230117220341262

关于4个指标的说明如下:

image-20230117220528894

我们重新运行修改过后的代码并且来到网页可视化工具中,点击alloc_space,可以看到

image-20230117221231193

有一部分代码也占用了很大的内存,这部分的代码在不断的申请大量内存并且回收,十分耗费性能,

scss
复制代码
func (d *Dog) Run() {
    log.Println(d.Name(), "run")
    _ = make([]byte, 16 * constant.Mi)
}

这里有个小插曲,如果尝试一下将 16 * constant.Mi 修改成一个较小的值,重新编译运行,会发现并不会引起频繁 GC,原因是在 golang 里,对象是使用堆内存还是栈内存,由编译器进行逃逸分析并决定,如果对象不会逃逸,便可在使用栈内存,但总有意外,就是对象的尺寸过大时,便不得不使用堆内存。所以这里设置申请 16 MiB 的内存就是为了避免编译器直接在栈上分配,如果那样得话就不会涉及到 GC 了。

注释掉相关代码,到这里关于内存的问题就都解决了。

goroutine协程

goroutine泄漏也会导致内存泄漏

使用命令:go tool pprof -http=:8080 http://localhost:6060/debug/pprof/goroutine

切换至火焰图,火焰图中:

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

image-20230117223237063

看得见是wolf中的Drink()函数占用goroutine最多,再打开source查看,就可以定位到代码的行数

同样我们把问题代码注释掉,问题解决。

mutex - 锁

虽然我们已经排查了部分问题代码,但是事情还没有完,我们需要进一步排查那些会导致程序运行慢的性能问题,这些问题可能并不会导致资源占用,但会让程序效率低下,这同样是高性能程序所忌讳的。

我们首先想到的就是程序中是否有不合理的锁的争用

go tool pprof -http=:8080 http://localhost:6060/debug/pprof/mutex命令可以排查锁的问题

image-20230117224140318

同样在定位到问题代码后注释掉

block - 阻塞

在程序中,除了锁的争用会导致阻塞之外,很多逻辑都会导致阻塞。

如果有不正常的或者可修改的阻塞还是需要排查一下的

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

image-20230117224903118

发现问题后注释掉。

两个block为什么只展示了一个

因为有默认的过滤策略,把一些占比特别小的函数结果过滤掉了。

2.2.3 pprof - 采样过程和原理

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

image-20230118104410520

image-20230118104445774

heap - 堆内存

image-20230118104610959

goroutine-协程和threadCreate-线程创建

image-20230118114436508

Block-阻塞和Mutex-锁

image-20230118115547201

2.3 性能调优案例

image-20230131091552964

2.3.1 业务服务优化

image-20230131091708762

image-20230131091629146

image-20230131092002190

image-20230131092221078

image-20230131093237589

使用火焰图来进行性能瓶颈的定位:

  • 使用库不规范
  • 高并发场景优化不足

image-20230131095029242

  • 规范组件库使用
  • 高并发场景优化
  • 增加代码检查规则避免增量劣化出现
  • 优化正确性验证

image-20230131095120805

image-20230131095213362

2.3.2基础库优化

image-20230131095537809

2.3.3 Go语言优化

image-20230131095805513

2.4 总结

image-20230131095902652

作者:半妖
链接:juejin.cn/post/719554…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。