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

55 阅读6分钟
  • 这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天

1. 重点内容

  • 编码规范
  • 性能优化

2. 知识点

高质量编码

  • 各种边界条件是否考虑完备
  • 异常处理,稳定性保证
  • 易读以维护
2.1 编码规范
  1. 代码格式

    • 使用gofmt自动格式化代码
  2. 注释

    • 注释应该解释代码的

      • 作用:适合注释公共符号

        // Open opens the named file for reading.
        func Open(name string) (*File, error) {
            return OpenFile(name, O_RDONLY, 0)
        }
        
      • 如何做的:适合注释实现过程

        // Add the Referer header from the most recent request URL
        // to the new one, if it's not https -> http:
        if ref := refererForURL(reqs[len(reqs)-1].URL, req.URL); ref != "" {
            req.Header.Set("Referer", ref)
        }
        
      • 实现的原因:适合解释代码的外部因素,提供额外上下文

      • 什么情况会出错:适合解释代码的限制条件

    • 公共符号始终要注释

      • 包中声明的每个公共的符号:变量、常量、函数和结构体
      • 任何不明显也不简短的公共功能
      • 库中的任何函数
      • 例外:实现接口的方法不需要注释
      • 若结构体注释已经说明它的函数的作用,则该函数不需要注释
  3. 命名规范

    • 简洁胜于冗长

      // bad
      for index := 0; index < len(s); index++ {
          // do something
      }
      ​
      // good
      for i := 0; i < len(s); i++ {
          // do something
      }
      
    • 缩略词全大写,当其位于变量的开头并不需要导出时,使用全小写

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

    • 函数名

      • 不携带包名的上下文信息,因为包名和函数名是成对出现的
    • 包名

      • 只由小写字母组成,不包含大写字母和下划线
      • 简短并包含一定上下文信息
      • 不与标准库同名
      • 尽量满足:不使用常用变量作为包名、使用单数、谨慎使用缩写
  4. 控制流程

    • 避免嵌套,保持正常流程

      // bad
      if foo {
          return x
      } else {
          return nil
      }
      ​
      //good
      if foo {
          return x
      }
      return nil
      
    • 尽量保持正常代码路径为最小缩进

      // bad
      func OneFunc() error {
          err := doSomething()
          if err == nil {
              err := doAnotherThing()
              if err == nil {
                  // normal case
                  return nil
              }
              return err
          }
          return err
      }
      ​
      //good
      func One Func() error {
          if err := doSomething(); err != nil {
              return err
          }
          if err := doAnotherThing(); err != nil {
              return err
          }
          // normal case
          return nil
      }
      
  5. 错误和异常处理

    • 简单错误

      • 简单的错误指的是仅出现一次的错误,且在其他地方不需要捕获该错误
      • 优先使用errors.New来创建匿名变量来直接表示简单错误
      • 如果有格式化的需求,使用fmt.Errorf
    • 错误的Wrap和Unwrap

      • 错误的包装提供了一个error嵌套另一个error的能力,生成一个error的跟踪链
      • fmt.Errorf中使用:%w关键字来将一个错误wrap至其错误链中
    • 错误判定

      • 判定是否为特定错误,使用errors.Is()。该方法可以判定错误链上的所有错误是否含有特定的错误
      • 在错误链上获取指定种类的错误,使用errors.As()
    • panicrecover

      • panic

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

        • recover只能被defer的函数使用
        • 嵌套无法生效
        • 只在当前的goroutine生效
        • defer的语句后进先出
        • 如果需要跟多上下文信息,可以recover后在log中记录当前的调用栈
2.2 性能优化

在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的性能 有时候时间效率和空间效率可能对立,此时应当分析那个更重要,作出适当的折衷。 针对Go语言编程,介绍Go相关的性能优化建议

  1. Benchmark

    • Go语言提供了支持基准性能测试的benchmark工具

      go test -bench=. -benchmen
      
  2. slice

    • 预分配内存

    • 大内存释放问题

      • 原切片由大量的元素构成,在原切片的基础上切片,虽然只使用了很小一段,但底层数组在内存中仍然占据了大量空间,得不到释放
      • 使用copy替代re-slice
  3. map

    • 预分配内存
  4. 字符串处理

    • 使用strings.Builder
    • 使用+拼接性能最差,strings.Builder btyes.Buffer相近,strings.Builder更快
  5. 空结构体

    • 空结构体实例不占据任何内存空间,作为占位符使用
  6. atomic

    • 保证计数准确,线程安全
2.3 性能优化工具
  1. 采样过程和原理

    • CPU

      • 采样对象:函数调用和它们占用的时间

      • 采样率:100次/秒,固定值

      • 采样时间:从手动启动到手动结束

      • 启动采样时,进程向OS注册一个定时器,OS会每隔10ms向进程发送一个SIGPROF信号, 进程接收到信号后就会对当前的调用栈进行记录。与此同时,进程会启动一个写缓冲的goroutine,它会每隔100ms从进程中读取已经记录的堆栈信息,并写入到输出流。
      • 当采样停止时,进程向OS取消定时器,不再接收信号,写缓冲读取不到新的堆栈时,结束输出
    • Heap

      • 采样程序通过内存分配器在堆上分配和释放的内存,记录分配/释放的大小和数量
      • 采样率:默认每分配512KB内存会采样一次, 采样率是可以在运行开头调整的,设为1则为每次分配都会记录
      • 采样时间:从程序运行到采样时
      • 采样指标:alloc_space,alloc_objects,inuse_space,inuse_objects
      • 计算方式:inuse = alloc - free
    • Goroutine&ThreadCreate

      • Goroutine

        • 记录所有用户发起且在运行中的goroutine(即入口非runtime开头的)runtime.main的调用栈的信息
      • ThreadCreate

        • 记录程序创建的所有系统线程的信息
    • Block & Mutex

      • 阻塞操作

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

        • 采样争抢锁的次数和耗时
        • 采样率:只记录固定比例的锁操作,1为每次加锁均记录
  2. 性能调优案例

    • 业务服务优化

      • 基本概念

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

        1. 建立性能评估手段

          • 服务性能评估方式

            • 单独benchmark无法满足复杂逻辑分析
            • 不同负载情况下性能表现差异
          • 请求流量构造

            • 不同请求参数覆盖的逻辑不同
            • 线上真实流量情况
          • 压测范围

            • 单机器压测
            • 集群压测
          • 性能数据采集

            • 单机性能数据
            • 集群性能数据
        2. 分析性能数据,定位性能瓶颈

          • 使用库不规范

            • 日志使用不规范:一部分是调试日志发布到线上,一部分是线上服务在不同的调用链路上数据有差别。到真实线上全量场景,这会导致日志量增加,影响性能
          • 高并发场景优化不足

        3. 重点优化项改造

          • 正确性是基础

            • 在变动较大的性能优化上线之前,还需要进行正确性验证,因为线上的场景和流程太多,所以要借助自动化手段来保证优化后程序的正确性
          • 响应数据diff

            • 线上请求数据录制回放
            • 新旧逻辑接口数据diff
        4. 优化效果验证

          • 重复压测验证

            • 压测并不能保证和线上表现完全一致,有时还要通过线上的表现再进行分析改进,是个长期的过程
          • 上线评估优化效果

            • 关注服务监控
            • 逐步放量
            • 收集性能数据
        5. 进一步优化,服务整体链路分析

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

      • AB实验的SDK优化

        • 分析基础库核心逻辑和性能瓶颈

          • 设计完善改造方案
          • 数据按需获取
          • 数据序列化协议优化
        • 内部压测验证

        • 推广业务服务落地验证

    • Go语言优化

      • 编译器&运行时的优化

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

        优点:

        • 接入简单,只需要调整编译配置
        • 通用性强