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

113 阅读6分钟

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

0. 内容概述

  1. 高质量编程
  2. Go 语言性能优化技巧
  3. Go 程序性能调优工具 pprof
  4. 了解工程中性能优化的原则和流程

1. 高质量编程

1.1 高质量编程简介

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

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

编程原则:

  • 简单性:以简单清晰的逻辑编写代码,消除多余的复杂性
  • 可读性:代码是写给人看的,可维护代码的第一步是确保代码可读
  • 生产力:团队整体工作效率非常重要

1.2 编码规范

1.2.1 格式化

gofmt:Go 语言官方提供的工具,能自动格式化 Go 语言代码为官方统一风格

goimports: 依赖包管理和字母序排序

1.2.2 注释

注释应该做的:

  • 解释代码作用(注释公共符号)
  • 解释代码如何做的(注释实现过程)
  • 解释代码实现的原因(注释外部因素、额外上下文)
  • 解释代码什么时候会出错(解释代码的限制条件)

代码是最好的注释,注释提供代码未表达出的上下文信息

1.2.3 命名规范

  • 变量命名
    • 简洁胜于冗长
    • 缩略词全大写,位于开头且不需要导出时全小写
    • 距离被使用地方越远,则需要携带越多的上下文信息
  • 函数命名
    • 不需要携带 package 的上下文信息,因为函数名和 package 总是成对出现
    • 函数名尽量简短
    • 当名为 foo 的包内某个函数返回类型 Foo 时,可以省略类型信息
    • 当名为 foo 的包内某个函数返回类型不是 Foo 时,可以在函数名中加入类型信息
  • 包命名
    • 只用小写字母组成,不用大写字母和下划线
    • 简短且包含一定上下文信息
    • 不要与标准库同名
    • 尽量不使用常用变量名
    • 尽量使用单数
    • 谨慎使用缩写

1.2.4 控制流程

  • 减少嵌套,保持正常流程清晰。如if中包含return,则可以去除冗余的else
  • 尽量保持正常代码路径为最小缩进。优先处理错误情况,减少分支嵌套
  • 故障问题大多出现在复杂的条件语句和循环语句中

1.2.5 错误和异常处理

  • 简单错误
    • 指仅出现一次的错误,其他地方不需要捕获该错误
    • 优先使用 errors.New 创建匿名变量来表示简单的错误
    • 格式化需求可使用 fmt.Errorf
  • 错误的 Wrap 和 Unwrap
    • 提供一个 error 嵌套另一个 error 的能力,生成一个 error 的跟踪链
    • fmt.Errorf中使用%w关键字来将一个错误关联到错误链中
    • errors.Is判断一个错误是否在错误链上含有特定的错误
    • errors.As在错误链上获取特定种类的错误
  • panic
    • 不建议在业务代码中使用panic
    • 调用函数不包含recover会造成程序崩溃
    • 程序启动阶段发生不可逆转的错误时,可以在initmain函数中使用panic
    • 若问题可以被屏蔽或解决,使用error即可
  • recover
    • 只能在defer函数中使用
    • 嵌套无法生效
    • 只在当前 goroutine 生效
    • 如需要更多的上下文信息,可以在recover后在 log 中记录当前的调用栈

1.3 性能优化建议

1.3.1 Benchmark

  • 性能表现需要实际数据衡量
  • Go 提供了支持基准性能测试的 Benchmark 工具

1.3.2 Slice

  • 使用 make() 初始化时,尽可能提供容量信息,预分配内存
  • 大切片新建小切片,使用copy()替代 re-slice 可以使得大切片的底层数组被释放;直接创建切片由于底层数组有引用,得不到释放

1.3.3 Map

  • 使用 make() 初始化时,尽可能提供容量信息,预分配内存
  • 不断向 map 中添加元素会触发扩容

1.3.4 字符串

  • 性能:strings.Builder 优于 bytes.buffer 优于直接相加
  • 使用 + 每次都会重新分配内存
  • strings.Builder,bytes.buffer 底层都是[]byte数组
  • Grow()方法在字符串长度已知情况下,预分配内存

1.3.5 空结构体

  • struct{} 实例不占用任何内存空间
  • 可以map[int]struct{}替代map[int]bool

1.3.6 atomic

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

2 性能调优

2.1 性能调优简介

性能调优原则:

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

2.2 性能分析工具 pprof

2.2.1 功能简介

  • 分析
    • 网页
    • 可视化终端
  • 展示
    • Top
    • 调用图-Graph
    • 火焰图-FlameGraph
    • Peek
    • 源码-Source
    • 反汇编-Disassemble
  • 工具
    • runtime/pprof
    • net/http/pprof
  • 采样
    • CPU
    • 堆内存-Heap
    • 协程-Goroutine
    • 锁-Mutex
    • 阻塞-Block
    • 线程创建-ThreadCreate

2.2.2 示例

  • CPU 分析
// 终端启动
go tool pprof "http://localhost:6060/debug/pprof/profile?second=10"

// 查看 CPU 占比 TopN
top

// 定位 CPU 占比代码行,正则匹配过滤
list /exp

// 调用关系可视化
web
  • Heap-堆内存
// web 启动,参数 http 表示打开可视化 web 界面
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
  • 火焰图
    • 由上到下代表调用顺序
    • 每一个块代表一个函数,越长代表占用 CPU 时间越长
    • 火焰图是动态的,支持点击块进行分析

2.2.3 采样过程和原理

  • CPU

    • 采样对象:函数调用及其占用时间
    • 采样率:100次每秒,固定值
    • 采样时间:手动启动到手动结束
    • 操作系统每 10 ms 向进程发送一次 SIGPROF 信号,进程每接收到 SIGPROF 信号会记录到调用堆栈,写缓冲每 100 ms 读取已经记录的调用栈并写入输出流
  • Heap 堆内存

    • 通过内存分配器在堆上分配和释放的内存,记录分配/释放的大小和数量
    • 采样率:每 512 KB 记录一次,可在运行开头修改
    • 采样时间:从程序运行开始到采样时
    • 采样指标:alloc_space、alloc_objects、inuse_space、inuse_objects
    • 计算方式:inuse=alloc-free
  • Goroutine 协程 & ThreadCreate 线程创建

    • Goroutine:记录用户发起的运行中的所有 goroutine(入口非 runtime 开头的)runtime.main 调用栈信息
    • ThreadCreate:记录程序创建的所有系统线程信息
  • Block 阻塞 & Mutex 锁

    • 阻塞操作
      • 采样阻塞操作的次数和耗时
      • 采样率:阻塞耗时超过阈值才会被记录,1 为每次阻塞均记录
    • 锁竞争
      • 采样争抢锁的次数和耗时
      • 采样率:只记录固定比例的锁操作,1为每次加锁均记录

总结

本节课主要从编码规范和性能优化建议两个方面讲述了 Go 语言如何实现高质量编程,同时对于 pprof 性能分析工具的用法和原理进行了说明。