go 高质量编程和性能优化 | 青训营

92 阅读6分钟

go 高质量编程

简介

高质量代码——正确可靠、简洁清晰的代码

  • 边界条件
  • 异常处理,保证稳定性
  • 易读易维护,便于合作处理

原则

  1. 简单性

    便于修复、改进;消除多余的复杂性

  2. 可读性

    可维护的第一步

  3. 生产力

    通过遵循常见的规范,便于提高团队整体效率

规范

Go Style | styleguide

  • 代码格式:推荐使用 gofmt自动格式化工具

  • 注释:

    解释代码 What, How, Why, When以及提示错误情况

    代码就是最好的注释

  • 命名规范

    1. 变量

      缩略语应全部大写;

      全局变量应携带更多信息;

    2. 函数名

      函数不用携带包的信息,因为总是一起出现

    3. 只是用小写字母;

      简短但包括一定信息;

      不要与标准库相同;

好的命名能够降低理解代码的成本;

结合上下文信息,设计简洁清晰的代码

  • 控制流程

    遵循线性原理,避免复杂的嵌套分支

  • 错误和异常处理

    1. 对于只出现一次的简单错误,通过error.New, fmt.Errorf就可以处理

    2. WrapUnwrap

      提供了error嵌套的能力,从而生成一个 error 的跟踪链

      fmt.Errorf中使用: %w 关键字来将一个错误关联到错误链中

      • 使用errors.Is进行错误判定
      • 使用errors.As取出指定类型的错误
    3. panic

      不建议在业务代码中使用 panic

    4. recover

      只能在 defer 的函数使用; 嵌套无法生效; 满足后进先出,使用时注意顺序

      可以 recover 后在 log 中记录当前的调用栈

性能优化建议

基准性能测试工具benchmarkgo test -bench=. -benchmem

会展示测试函数名称,执行次数,每次执行花费时间,申请内存大小,申请内存次数

slice

  1. 在使用 make()初始化slice时,提供容量信息,减少内存分配次数

对 slice 进行扩容时,如果原数组容量不足,则会先进行扩容操作,这会有很大的开销 2023-07-29-22-04-32-image.png

  1. 由于切片对源数组的引用,导致源数组得不到释放

    此时可以使用copy代替重新创建切片

map

  1. 同样应该预分配内存(简单预估)

字符串处理

使用strings.Builderbytes.Buffer代替直接使用+号拼接字符串

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

空结构体

空结构体不会占用内存,起到占位的作用,同时具有一定的语义信息

例如,如果使用 map 代替 set 的实现,只需要使用 key,此时就可以将 value 设置为空结构体

atomic 包

多线程编程时,使用 atomic 包来保护一个单独的变量,而不使用锁

  • 锁的实现是通过操作系统来实现 , 属于系统调用
  • atomic 操作(即原子指令)通过硬件实现,效率比锁高

总结

不要一味追求性能,越高级的性能优化方法,越容易出现问题,最重要的是满足正确可靠简洁清晰的质量要求的前提

性能调优实战

原则

  • 依靠数据而不是猜测
  • 定位性能瓶颈而不要追究细枝末节
  • 不要过早优化,需要等到系统稳定后再分析
  • 不要过度优化,容易影响后续迭代

优化最重要的是保证程序正确性

工具——pprof

能知道应用在那些地方耗费了多少系统资源,并且可以可视化分析

功能简介

2023-07-31-15-10-24-image.png

原理

CPU

2023-07-31-15-17-45-image.png

  • 操作系统:每10ms向进程发送一次SIGPROF信号
  • 进程:每次接收到SIGPROF记录调用堆栈
  • 写缓冲:每100ms读取以及记录的调用栈并写入输出流

Heap 堆内存

  • 采样程序:通过内存分配器在堆上分配和释放的内存,记录分配/释放的大小和数量
  • 采样率:每分配 512KB 记录一次,可在运行开头修改,1 为每次分配均记录
  • 采样时间:从程序运行开始到采样开始
  • 采样指标:alloc space(分配空间), alloc_objects(分配对象), inuse_space(停止空间),inuse_objects(停止对象)
  • 计算方式: inuse = alloc - free

协程与线程创建

  • 协程:记录所有用户发起且在运行中(入口非runtime)的协程的调用栈信息
  • 线程:记录程序创建的所有系统线程的信息

2023-07-31-15-28-29-image.png

阻塞和锁

  • 阻塞

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

2023-07-31-15-30-00-image.png

  • 锁竞争

    • 采样争抢锁的次数和耗时
    • 采用率:只记录固定比例的锁操作,1为全部记录

2023-07-31-15-31-18-image.png

实际案例分析

流程

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

建立性能评估手段

  1. 服务性能评估方式

    • 单独使用benchmark无法满足复杂逻辑分析
    • 需要在不同负载下分析性能表现
  2. 请求流量构造

    • 不同请求参数不同,覆盖的逻辑不同
    • 线上的真实流量不同
  3. 压力测试需要进行单机测试和集群测试

  4. 性能数据采集也需要单机和集群性能数据

分析性能数据,定位性能瓶颈

获取到数据之后开始进行分析

  1. 使用库不规范导致产生多余预期的资源消耗,影响了服务性能
  2. 高并发场景优化不足,对对应逻辑进行优化

重点优化项改造

需要保证正确性

  • 将线上请求的返回值录制保存下来
  • 使用相同的请求值再次请求优化后的服务
  • 对比返回值是否相同

如果返回值相同,则此次优化对功能的正确性无影响

优化效果验证

重复进行压力测试验证,查看优化效果

  • 关注服务监控
  • 逐步放量:逐步增加请求量,便于定位问题
  • 收集性能数据

进一步优化,服务整体链路分析

  • 规范上游接口,明确场景需求和数据集
  • 分析链路,通过业务流程优化提升服务性能