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

82 阅读6分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第3篇笔记。

1、 学习目标

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

2、 高质量编程

高质量代码的标准:

  • 正确性:是否考虑各种边界条件
  • 可靠性:异常情况或者错误的处理策略是否明确,稳定性保证
  • 易读易维护

编程原则

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

简单性

  • 消除“多余的复杂性”,以简单清晰的逻辑编写代码
  • 不理解的代码无法修复改进

可读性

  • 代码是写给人看的,而不是机器
  • 编写可维护代码的第一步是确保代码可读

生产力

  • 团队整体工作效率非常重要

​ —— Go 语言开发者 Dave Cheney

2.1 编码规范

  • 代码格式 推荐使用 gofmt 自动格式化代码
  • 注释 解释代码作用

解释代码如何做的

解释代码实现的原因

解释代码什么情况会出错

  • 命名规范 简洁胜于冗长

缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写

例如使用 ServeHTTP 而不是 ServeHttp
使用 XMLHTTPRequest 或者 xmlHTTPRequest

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

全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义
  • 控制流程 避免嵌套,保持正常流程清晰

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

image.png

尽量保持正常代码路径为最小缩进,优先处理错误情况/特殊情况,尽早返回或继续循环来减少嵌套

image.png

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

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

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

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

2.2 性能优化

高质量的代码能够完成功能,但是在大规模程序部署的场景,仅仅支持正常功能还不够,我们还要尽可能地提升性能,节省资源成本。接下来主要介绍性能相关的建议。

  • 性能优化的前提是满足正确可靠、简洁清晰等质量因素
  • 性能优化是综合评估,有时候时间效率和空间效率可能对立,需要作出适当的折衷
  • 针对 Go 语言特性,介绍 Go 相关的性能优化建议

2.2.1 Benchmark

Go 语言提供了支持基准线性能测试的 benchmark 工具,通过命令 go test -bench=. -benchmen 运行 benchmark 可以得到测试结果

image.png

image.png

2.2.2 Slice

Slice 预分配内存

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

image.png

  • 切片本质是一个数组片段的描述

    • 包括数组指针
    • 片段的长度
    • 片段的人容量(不改变内存分配情况下的最大长度)
  • 切片操作并不复制切片指向的元素

  • 创建一个新的切片会复用原来切片的底层数组

image.png

以切片的 append 为例,append时有两种场景:

  • 当 append 之后的长度小于等于 cap,将会直接利用原底层数组剩余的空间。
  • 当 append 后的长度大于 cap 时,则会分配一块更大的区域来容纳新的底层数组。

image.png

使用 slice 的一个陷阱:大内存未释放:

在已有切片基础上创建切片,不会创建新的底层数组。

因此可能出现这样一种情况:原切片由大量的元素构成,但是我们在原切片的基础上切片,虽然只使用了很小一段,但底层数组在内存中仍然占据了大量空间,得不到释放。

这种情况可以使用 copy 替代 re-slice

2.2.3 Map

Map预分配内存

  • 不断向 map 中添加元素的操作会触发 map 的扩容
  • 提前分配好空间可以减少内存拷贝和 Rehash 的消耗
  • 建议根据实际需求提前预估好需要的空间

image.png

2.2.4 字符串处理

strings.Builder 和 bytes.Buffer

  • 字符串在 Go 语言中是不可变类型。占用内存大小是固定的
  • 使用 + 每次都会重新分配内存
  • strings.Builder, bytes.Buffer 底层都是 []byte 数组
  • 内存扩容策略,不需要每次拼接重新分配内存

image.png

strings.Builder 比 bytes.Buffer 更快:

  • bytes.Buffer 转化为字符串时重新申请了一块空间
  • strings.Builder 直接将底层的 []byte 转化成了字符串类型返回

image.png

image.png

2.2.5 空结构体

空结构体是节省内存空间的一个手段

  • 空结构体 struct{} 实例不占据任何的内存空间

  • 可作为各种场景下的占位符使用

    • 节省资源

    • 空结构体本身具有很强的语义,即这里不需要任何值,仅作为占位符

image.png 空结构体的实用场景:实现 set

  • 这个场景只需要用到 map 的键,而不需要值
  • 即使是将 map 的值设置为 bool 类型,也会多占据 1 个字节空间

2.2.6 Atomic包

在多线程的场景下,比如实现一个多线程共用的计数器,如何保证计数准确,线程安全,有不同的方式。

image.png

可以看到 Atomic 包比加锁的性能更好,原因如下

  • 锁的实现是通过操作系统来实现,属于系统调用
  • atomic 操作是通过硬件实现,效率比锁高
  • sync.Mutex 应该用来保护一段逻辑,不仅仅用于保护一个变量
  • 对于非数值操作,可以使用 atomic.Value,能承载一个 interface{}

3、 性能调优

调优原则:

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

3.1 性能分析工具pprof

image.png

3.2 构建pprof项目

下载项目代码:github.com/wolfogre/go…

项目提前埋入了一些炸弹代码,产生可观测的性能问题。

会占用 1CPU 核心和超过 1GB 的内存