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

84 阅读8分钟

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

高质量编程

高质量编程简介

  • 代码正确可靠、简洁清晰
  • 各种边界条件是否考虑完备
  • 异常情况处理,稳定性包装
  • 易读易维护

原则

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

可读性

  • 代码可阅读性强
  • 编写可维护代码的第一步是确保代码可读

生产力

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

编码规范

代码格式

推荐使用gofmt自动格式化代码
goimports也是官方提供工具,可以管理依赖包

注释

  • 代码作用
  • 代码如何做
  • 解释实现原因 解释代码外部因素 提供额外上下文
  • 解释代码什么情况出错 适合解释代码限制条件
  • 公共符号始终要注释 公共(遍历、函数...) 接口不需要 总结
  • 代码是最好的注释
  • 注释应该提供代码未表达出的上下文信息

命名规范

variable

  • 简洁胜于冗长
  • 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
  • 变量距离其被使用的地方越远,则需要携带越多的上下文信息
  • 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义

function

  • 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的
  • 函数名尽量简短
  • 当名为 foo 的包某个函数返回类型 Foo 时,可以省略类型信息而不导致歧义
  • 当名为 foo 的包某个函数返回类型 T 时(T 并不是 Foo),可以在函数名中加入类型信息

package

  • 只由小写字母组成。不包含大写字母和下划线等字符
  • 简短并包含一定的上下文信息。例如 schema、task 等
  • 不要与标准库同名。例如不要使用 sync 或者 strings 总结
  • 降低阅读理解代码成本
  • 重点考虑上下文信息,设计简洁清晰的名称

控制流程

  • 避免嵌套,保证正常流程清晰
  • 两个分支都包含return,可以取出冗余的else
  • 尽量保持正常代码路径为最小缩进,优先处理错误情况/特殊情况,并尽早返回或继续循环来减少嵌套,增加可读性 总结
  • 线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支
  • 正常流程代码沿屏幕向下移动
  • 提升代码可维护性和可读性

错误和异常处理

  • 简单错误处理

    • 优先使用 errors.New 来创建匿名变量来直接表示该错误。有格式化需求时使用 fmt.Errorf
  • 错误的 Wrap 和 Unwrap

    • 在 fmt.Errorf 中使用 %w 关键字来将一个错误 wrap 至其错误链中
  • 错误判定

    • 使用 errors.Is 可以判定错误链上的所有错误是否含有特定的错误。
    • 在错误链上获取特定种类的错误,使用 errors.As
  • panic

    • 不建议在业务代码中使用 panic
    • 如果当前 goroutine 中所有 deferred 函数都不包含 recover 就会造成整个程序崩溃
    • 当程序启动阶段发生不可逆转的错误时,可以在 init 或 main 函数中使用 panic
  • recover

    • recover 只能在被 defer 的函数中使用,嵌套无法生效,只在当前 goroutine 生效
    • 如果需要更多的上下文信息,可以 recover 后在 log 中记录当前的调用栈。 总结
  • panic 用于真正异常的情况

  • error 尽可能提供简明的上下文信息,方便定位问题

  • recover 生效范围,在当前 goroutine 的被 defer 的函数中生效

补充defer

  • defer会在函数返回前调用
  • 多个defer语句是后进先出
if true {
   defer fmt.Print("1")
} else {
   defer fmt.Print("2")
}
defer fmt.Print("3")
//结果: 31

性能优化建议

  • 在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的效率

slice预分配内存

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

原因

  • 切片底层原理还是数组,提前指定容量可以减少扩容带来的内存时间的损耗。
  • 陷阱:如果在已有大切片下创建切片,不会去释放原有大切片空间,所以推荐创建新的切片,用copy代替re-slice
ogrin[len(origin)-2:]  //bad

copy(new,ogrin[len(origin)-2:]) //good

map预分配内存

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

字符串处理

  • 直接+ 最慢
  • stirngs.Builder 最快
  • bytes.Buffer 较快

使用空结构体节省内存

  • 空结构体struct{}实例不占据任何内存空间
  • 可作为占位符使用

使用atomic包

  • 锁的实现是通过操作系统来实现,属于系统调用,atomic 操作是通过硬件实现的,效率比锁高很多
  • sync.Mutex 应该用来保护一段逻辑,不仅仅用于保护一个变量
  • 对于非数值系列,可以使用 atomic.Value,atomic.Value 能承载一个 interface{} 总结
  • 避免常见的性能陷阱可以保证大部分程序的性能
  • 针对普通应用代码,不要一味地追求程序的性能
  • 应当在满足正确可靠、简洁清晰等质量要求的前提下提高程序性能
  • 越高级的性能优化手段越容易出现问题

性能调优实战

性能优化简介

原则

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

性能分析工具pprof实战

pprof是用于可视化和分析性能分析数据的工具 image.png

实践命令

排查 CPU 问题

pprof采样过程及原理审理

可参考使用 golang pprof 实战

性能调优案例

基本概念

  • 服务:能单独部署,承载一定功能的程序
  • 依赖:Service A 的功能实现依赖 Service B 的响应结果,称为 Service A 依赖 Service B
  • 调用链路:能支持一个接口请求的相关服务集合及其相互之间的依赖关系
  • 基础库:公共的工具包、中间件

业务服务优化

流程

  • 建立服务性能评估手段
    • 服务性能评估
      • 单独benchmark无法满足复杂逻辑分析
      • 不同负载情况下性能表现差异大
    • 请求流量构造
      • 不同请求参数覆盖逻辑不同
      • 线上真实流量情况
    • 压测范围
      • 单机器压测
      • 集群压测
    • 性能数据采集
      • 单机性能数据
      • 集群性能数据
  • 分析性能数据,定位性能瓶颈
    • 使用库不规范
    • 日志使用不规范
    • 高并发常见优化不足
    • ......
  • 重点优化项改造
    • 正确性是基础
    • 响应数据diff
      • 线上请求数据录制回放
      • 新旧逻辑接口数据diff
  • 优化效果验证
    • 重复压测验证
    • 上线评估优化效果
      • 关注服务监控
      • 逐步放量
      • 收集性能数据
  • 进一步优化,服务整体链路分析
    • 规范上游服务调用接口,明确场景需求
    • 分析链路,通过业务流程优化提升服务性能

基础库优化

AB实验SDK优化

AB实验:即将模拟分为两组用户对比不同策略进行实验对比

  • 分析基础库核心逻辑和性能瓶颈
    • 设计完善改造方案
    • 数据按需获取
    • 数据序列化协议优化
  • 内部压测验证
  • 推广业务服务落地验证

Go语言优化

编译器&运行时优化

  • 优化内存分配策略
  • 优化代码编译流程,生成更高效的程序
  • 内部压测验证
  • 推广业务服务落地验证

优点:

  • 接入简单,只需要调整编译配置
  • 通用性强 总结
  • 性能调优原则:依靠数据而不是猜测
  • 性能分析工具pprof
    • 熟练使用pprof工具排查性能问题了解其基本原理
  • 性能调整
    • 保证正确性
    • 定位主要瓶颈