这是我参与「第三届青训营 -后端场」笔记创作活动的的第3篇笔记。
1、 学习目标
- 如何编写更简洁清晰的代码
- 常用 Go 语言程序优化手段
- 熟悉 Go 程序性能分析工具
- 了解工程中性能优化的原则和流程
2、 高质量编程
高质量代码的标准:
- 正确性:是否考虑各种边界条件
- 可靠性:异常情况或者错误的处理策略是否明确,稳定性保证
- 易读易维护
编程原则
实际应用场景千变万化,各种语言的特性和语法各不相同,但是高质量编程遵循的原则是相通的。
简单性
- 消除“多余的复杂性”,以简单清晰的逻辑编写代码
- 不理解的代码无法修复改进
可读性
- 代码是写给人看的,而不是机器
- 编写可维护代码的第一步是确保代码可读
生产力
- 团队整体工作效率非常重要
—— Go 语言开发者 Dave Cheney
2.1 编码规范
- 代码格式 推荐使用 gofmt 自动格式化代码
- 注释 解释代码作用
解释代码如何做的
解释代码实现的原因
解释代码什么情况会出错
- 命名规范 简洁胜于冗长
缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
例如使用 ServeHTTP 而不是 ServeHttp
使用 XMLHTTPRequest 或者 xmlHTTPRequest
变量距离其被使用的地方越远,则需要携带越多的上下文信息
全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义
- 控制流程 避免嵌套,保持正常流程清晰
如果两个分支中都包含 return 语句,则可以去除冗余的 else。
尽量保持正常代码路径为最小缩进,优先处理错误情况/特殊情况,尽早返回或继续循环来减少嵌套
最常见的正常流程的路径被嵌套在两个 if 条件内
成功的退出条件是 return nil,必须仔细匹配大括号才能发现
函数最后一行返回一个错误,需要追溯到匹配的左括号,才能了解何时会触发错误
如果后续正常流程需要增加一步操作,调用新的函数,则又会增加一层嵌套
2.2 性能优化
高质量的代码能够完成功能,但是在大规模程序部署的场景,仅仅支持正常功能还不够,我们还要尽可能地提升性能,节省资源成本。接下来主要介绍性能相关的建议。
- 性能优化的前提是满足正确可靠、简洁清晰等质量因素
- 性能优化是综合评估,有时候时间效率和空间效率可能对立,需要作出适当的折衷
- 针对 Go 语言特性,介绍 Go 相关的性能优化建议
2.2.1 Benchmark
Go 语言提供了支持基准线性能测试的 benchmark 工具,通过命令 go test -bench=. -benchmen 运行 benchmark 可以得到测试结果
2.2.2 Slice
Slice 预分配内存
尽可能在使用 make() 初始化切片时提供容量信息
-
切片本质是一个数组片段的描述
- 包括数组指针
- 片段的长度
- 片段的人容量(不改变内存分配情况下的最大长度)
-
切片操作并不复制切片指向的元素
-
创建一个新的切片会复用原来切片的底层数组
以切片的 append 为例,append时有两种场景:
- 当 append 之后的长度小于等于 cap,将会直接利用原底层数组剩余的空间。
- 当 append 后的长度大于 cap 时,则会分配一块更大的区域来容纳新的底层数组。
使用 slice 的一个陷阱:大内存未释放:
在已有切片基础上创建切片,不会创建新的底层数组。
因此可能出现这样一种情况:原切片由大量的元素构成,但是我们在原切片的基础上切片,虽然只使用了很小一段,但底层数组在内存中仍然占据了大量空间,得不到释放。
这种情况可以使用 copy 替代 re-slice
2.2.3 Map
Map预分配内存
- 不断向 map 中添加元素的操作会触发 map 的扩容
- 提前分配好空间可以减少内存拷贝和 Rehash 的消耗
- 建议根据实际需求提前预估好需要的空间
2.2.4 字符串处理
strings.Builder 和 bytes.Buffer
- 字符串在 Go 语言中是不可变类型。占用内存大小是固定的
- 使用 + 每次都会重新分配内存
- strings.Builder, bytes.Buffer 底层都是 []byte 数组
- 内存扩容策略,不需要每次拼接重新分配内存
strings.Builder 比 bytes.Buffer 更快:
- bytes.Buffer 转化为字符串时重新申请了一块空间
- strings.Builder 直接将底层的 []byte 转化成了字符串类型返回
2.2.5 空结构体
空结构体是节省内存空间的一个手段
-
空结构体 struct{} 实例不占据任何的内存空间
-
可作为各种场景下的占位符使用
-
节省资源
-
空结构体本身具有很强的语义,即这里不需要任何值,仅作为占位符
-
空结构体的实用场景:实现 set
- 这个场景只需要用到 map 的键,而不需要值
- 即使是将 map 的值设置为 bool 类型,也会多占据 1 个字节空间
2.2.6 Atomic包
在多线程的场景下,比如实现一个多线程共用的计数器,如何保证计数准确,线程安全,有不同的方式。
可以看到 Atomic 包比加锁的性能更好,原因如下
- 锁的实现是通过操作系统来实现,属于系统调用
- atomic 操作是通过硬件实现,效率比锁高
- sync.Mutex 应该用来保护一段逻辑,不仅仅用于保护一个变量
- 对于非数值操作,可以使用 atomic.Value,能承载一个 interface{}
3、 性能调优
调优原则:
- 要依靠数据不是猜测
- 要定位最大瓶颈而不是细枝末节
- 不要过早优化
- 不要过度优化
3.1 性能分析工具pprof
3.2 构建pprof项目
下载项目代码:github.com/wolfogre/go…
项目提前埋入了一些炸弹代码,产生可观测的性能问题。
会占用 1CPU 核心和超过 1GB 的内存