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

46 阅读6分钟

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

高质量编程

  • 编写的代码能够表达正确可靠、间接清晰的目标

    • 各种边界条件是否考虑完备
    • 异常情况处理、稳定性保证
    • 易读易维护
  • 编程原则

    • 简单性
    • 可读性
    • 生产力
  • 编码规范

    • 代码格式

      • 使用gofmt自动格式化代码,保证所有的Go代码与官方推荐格式保持一致
    • 注释

    • 命名规范

      • variable

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

        • 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的
        • 函数名尽量简短
      • package

        • 只有小写字母组成,不包含大写字母和下划线等字符
        • 简短并包含一定的上下文信息
        • 不要与标准库同名

      关于命名大多数规范核心在于上下文

    • 控制流程

      • 避免嵌套,保持正常流程清晰
      • 尽量保持正常代码路径的最小缩进,优先处理错误情况/特殊情况,并尽早返回或继续循环来减少嵌套,增加可读性

      线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支,提高代码可读性

    • 错误和异常处理

      • 简单的错误处理

        • 优先使用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的函数中生效

性能调优实战

  • Benchmark
go test -bench=. -benchmem
  • slice 预分配内存

    • 原理

      • 切片本质是一个数组片段的描述,包括了数组的指针,这个片段的长度和容量(不改变内存分配情况下的最长长度)

      • 切片操作并不复制切片指向的元素,创建一个新的切片会复用原来切片的底层数组,因此切片操作时非常高效的

      • 切片有三个属性,指针(ptr)、长度(len)和容量(cap)。append有两种场景:

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

      因此,为了避免内存发生拷贝,如果能够知道最终的切片的大小,预先设置cap的值能够获得更好的性能

    • 陷阱:大内存未释放

      • 在已有切片基础上创建切片,不会创建新的底层数组
      • 原切片较大,代码在原切片的基础上新建小切片
      • 原底层数组在内存中应用,得不到释放
      • 可以使用copy代替re-slice
  • map预分配内存

    • 不断像map中添加元素的操作会触发map的扩容
    • 根据实际情况提前估算好需要的时间
    • 提前分配好空间可以减少内存拷贝和Rehash的消耗
  • 字符串处理

    • 使用strings.Builder

      • 使用+拼接性能最差,strings.Builder, bytes.Buffer相近,strings.Builder更快
    • 分析

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

    • 空结构体节省内存

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

      • 可以作为哥哥场景下占位符使用

        • 节省资源
        • 空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符
      • 实现Set,可以考虑用map来代替

      • 即使是将map的值设置为bool类型,也会多占据1个字节空间

  • atomic包

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

性能调优原则

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

pprof工具

  • 工具 Tool

    • runtime/pprof
    • net/http/pprof
  • 采样 Sample

    • CPU
    • 堆内存 Heap
    • 协程 Goroutine
    • 锁 Mutex
    • 阻塞 Block
    • 线程创建 ThreadCreate
  • 展示 View

    • Top
    • 调用图 Graph
    • 火焰图 FlameGraph
    • Peek
    • 源码 Source
    • 反汇编 Disassemble
  • 分析 Profile

    • 网页
    • 可视化终端
  • 实际分析排查过程

  • 排查CPU问题

    • 命令行分析
    • top命令
    • list命令
    • 熟悉web页面分析
    • 调用关系图、火焰图
    • 排查堆内存问题
    • 排查协程问题
    • 排查锁问题
    • 排查堵塞问题
  • pprof的采样过程和原理

    • CPU采样
    • 堆内存采样
    • 协程和系统线程采样
    • 阻塞操作和锁竞争采样

性能调优案例

  • 基本概念

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

    • 流程

      • 建立服务性能评估手段
      • 分析性能数据,定位性能瓶颈
      • 重点优化项改造
      • 优化效果验证
    • 建立压评测估链路

      • 服务性能评估
      • 构造请求流量
      • 压测范围
      • 性能数据采集
    • 分析性能火焰图,定位性能瓶颈

    • 重点优化项分析

      • 规范组件库使用
      • 高并发场景优化
      • 增加代码检查规范避免增量劣化出现
      • 优化正确性验证
    • 上线验证评估

      • 逐步放量
    • 进一步优化,服务整体链路分析

      • 规范上有服务调用接口,明确场景需求
      • 分析业务流程,通过业务流程优化提升服务性能