高质量编程与性能调优实战
这是我参与「第三届青训营 -后端场」笔记创作活动的的第3篇笔记
课程主要分为两个部分,分别是高质量编程和性能调优实战。其中高质量编程多是一些指导性原则,辅以一些示例进行讲解,加上课程PPT以及导学链接中已经总结得很清晰完善,所以本篇笔记仅记录一些个人的知识点记忆以及列出不太熟悉的知识要点。
高质量编程
高质量代码
编写的代码能够达到正确可靠、简洁清晰的目标可称之为高质量代码
- 各种边界条件是否考虑完备
- 异常情况处理,稳定性保证
- 易读易维护
编程原则
-
简单性
- 消除“多余的复杂性”,以简单清晰的逻辑编写代码
- 不理解的代码无法修复改进
-
可读性
- 代码是给人看的,而不是机器
- 编写可维护代码的第一步是确保代码可读
-
生产力
- 团队整体工作效率非常重要
编码规范
-
代码格式
- 推荐使用gofmt自动格式化代码
- gofmt:Go语言官方提供的工具,能自动格式化Go语言代码为官方统一风格
- goimports:Go语言官方提供的工具,实际等于gofmt加上依赖包管理,自动增删依赖的包引用、将依赖包按字母序排序并分类
-
注释的应用
- 解释代码的功能,描述公共符号的用途。
- 描述代码的实现过程,说明公共符号的实际实现方法。
- 阐述代码的原因,提供外部因素的背景信息,为代码提供额外上下文。
- 指出代码可能出错的情况,限制条件的说明。
-
公共符号的注释
- 所有公共符号都必须注释,包括在包中声明的变量、常量、函数以及结构等。
- 对于不明显或简短的公共功能,同样需要添加注释。
- 不论函数在库中的长度或复杂程度如何,都必须进行注释。
命名规范
-
变量
- 简洁的命名优于冗长的命名。
- 缩略词通常使用全大写,但如果缩略词在变量开头且不需要导出,则使用全小写。
- 变量与其使用位置的距离越远,需要携带越多的上下文信息。
-
函数
- 函数名不需要携带包名的上下文信息,因为包名和函数名总是成对出现。
- 函数名应该简短明了。
- 若某个包内名为 "foo" 的函数返回类型 "Foo",可以省略类型信息以避免歧义。
- 若某个包内名为 "foo" 的函数返回类型 "T",可以在函数名中加入类型信息。
-
包
- 包名只能由小写字母组成,不包含大写字母和下划线等字符。
- 包名应该简短且包含一定的上下文信息,例如 "schema"、"task" 等。
- 避免与标准库重名,例如不要使用 "sync" 或 "strings"。
- 避免使用常用变量名作为包名,例如使用 "bufio" 而不是 "buf"。
- 使用单数形式而非复数。
- 对缩写的使用要谨慎。
-
控制流程
- 避免过多的嵌套,保持正常流程的清晰性。
- 如果两个分支都包含 return 语句,可以去掉多余的 else 分支。
- 尽量保持正常代码路径的最小缩进。
- 优先处理错误和特殊情况,以减少嵌套。
- 尽量保持代码逻辑的线性,避免复杂的嵌套分支。
- 正常流程代码应该顺着屏幕向下移动,以提高可读性和可维护性。
- 这些原则将有助于提升代码的可维护性和可读性,同时减少故障问题在复杂条件和循环中的出现。
-
错误和异常处理
- 对于简单错误,如仅出现一次且不需要在其他地方捕获的错误,使用
errors.New创建匿名变量表示。 - 如果需要格式化,可以使用
fmt.Errorf。 - 错误的 Wrap 和 Unwrap 机制可以创建错误的跟踪链,通过
%w关键字关联错误。 - 使用
errors.Is判定特定错误,而非使用==,这样可以判定错误链上是否包含特定错误。 - 在错误链中获取特定种类的错误,使用
errors.As。 - 避免在业务代码中使用
panic,除非是启动阶段的不可逆转错误。 recover只在被defer的函数中有效,不支持嵌套,只在当前 Goroutine 生效。defer语句遵循后进先出的原则。- 如果需要更多上下文信息,可以在
recover后记录当前调用栈。
- 对于简单错误,如仅出现一次且不需要在其他地方捕获的错误,使用
性能优化
-
性能基准测试命令:
go test -bench=. -benchmem -
性能优化建议:
- 对于切片,应预先分配内存。
- 在使用
make()初始化切片时,尽量提供容量信息。
-
另一个潜在问题是未释放大内存:
- 基于已有切片创建新切片不会产生新的底层数组。
- 场景:原切片较大,代码在原切片基础上创建小切片。
- 原底层数组因仍被引用而无法释放。
- 建议使用
copy代替re-slice。
-
在使用map时,应预先分配内存。
- 频繁地向map中添加元素会触发map的扩容。
- 预先分配空间可以减少内存拷贝和Rehash的开销。
- 根据实际需求提前预估所需空间是明智之举。
-
字符串处理:
- 使用
+拼接性能较差,strings.Builder和bytes.Buffer相似,但strings.Builder更快。 - 在Go语言中,字符串是不可变类型,占用固定内存。
- 使用
+时每次重新分配内存。 strings.Builder和bytes.Buffer底层均是[]byte数组。bytes.Buffer转化为字符串时重新分配了一块内存。strings.Builder直接将底层的[]byte转换为字符串类型。- 内存扩容策略避免了每次拼接都重新分配内存。
- 使用
strings.Builder进行字符串拼接。
- 使用
-
利用空结构体节省内存:
- 使用空结构体
struct{}可以节省内存。 - 空结构体实例不占用内存空间。
- 可用作各种场景下的占位符,节省资源。
- 空结构体具备明确的语义,表示这里不需要任何值,只作为占位符。
- 在实现Set时,可用map来代替。
- 对于该场景,只需使用map的键,无需值。
- 即使将map的值设为bool类型,仍会占用1个字节空间。
- 使用空结构体
-
使用
atomic包:- 锁是通过操作系统实现的,涉及系统调用。
atomic操作是通过硬件实现的,效率高于锁。sync.Mutex应用于保护一段逻辑,不仅限于保护单个变量。- 对于非数值操作,可使用
atomic.Value,能够容纳interface{}类型。
性能调优实战
性能调优原则:
- 数据驱动优化:基于实际数据进行调优,而不是仅仅猜测或臆测。
- 定位瓶颈:重点关注最大的瓶颈,不要过多关注细枝末节。
- 避免过早优化:在了解问题之前不要进行过度的优化。
- 避免过度优化:不要花费过多精力在微小的性能改进上。
性能分析工具pprof:
pprof是一款用于可视化和分析性能数据的工具,它提供了多种功能来帮助定位和解决性能问题。
-
火焰图:
- 以垂直的方式表示函数调用顺序。
- 每个方块代表一个函数,方块的长度与该函数占用的CPU时间成比例。
- 火焰图是交互式的,点击方块可以进一步分析函数的性能。
-
采样过程与原理:
-
CPU采样:
- 采样对象:记录函数调用及其占用的时间。
- 采样率:每秒采样100次,固定值。
- 采样时间:从手动启动到手动结束。
-
堆内存采样:
- 通过内存分配器追踪堆上的内存分配和释放,记录大小和数量。
- 采样率:每分配512kb记录一次,可在运行开始时进行修改,1表示每次分配都记录。
- 采样时间:从程序运行开始到采样时。
- 采样指标:分配空间、分配对象、已使用空间、已使用对象。
- 计算方式:已使用 = 分配 - 释放。
-
Goroutine采样:
- 记录所有正在运行的用户发起的goroutine的调用栈信息,以及runtime.main的调用栈。
-
ThreadCreate采样:
- 记录程序创建的所有系统线程信息。
-
阻塞采样:
- 采样阻塞操作的次数和耗时。
- 采样率:只记录耗时超过阈值的阻塞操作,1表示每次阻塞都记录。
-
锁采样:
- 记录争夺锁的次数和耗时。
- 采样率:只记录固定比例的锁操作,1表示每次加锁都记录。
-
-
性能调优案例
业务服务优化:
- 一个“服务”是一个能够独立部署的程序,它承载着特定的功能。
- 当一个服务的功能实现需要依赖另一个服务的响应结果时,称为一个服务依赖另一个服务。
- “调用链路”是指一组相关的服务,它们一起支持一个接口请求,并且它们之间存在依赖关系。
- “基础库”是指公共的工具包和中间件。
基础库优化:
-
优化AB实验SDK。
-
这包括分析基础库核心逻辑以及性能瓶颈。
- 在分析的基础上,设计改进方案。
- 根据需要获取数据,避免不必要的数据获取。
- 优化数据序列化协议。
-
通过内部的压力测试来验证这些优化。
-
在业务服务中推广并验证这些优化。
-
Go语言优化:
-
对编译器和运行时进行优化。
- 为了优化内存分配,重新考虑内存分配策略。
- 优化代码的编译流程,以生成更高效的程序。
- 同样,通过内部压力测试来验证这些优化。
- 最终,将这些优化推广到业务服务中,并在实际落地中验证它们的效果。
-
Go语言的优势包括:
- 简单的接入方式,只需要调整编译配置。
- 高度的通用性,适用于各种应用场景。