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

62 阅读4分钟

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

高质量编程

简介

高质量:

  • 边界条件考虑完备
  • 异常情况处理
  • 易读易维护

编程原则:

  • 简单性: 消除多余的复杂性
  • 可读性
  • 生产力: 效率

编码规范

高质量的代码: 代码格式, 注释, 命名规范, 控制流程, 错误和异常处理

  • 代码格式: 使用gofmt进行自动格式化, 使用goimports进行依赖包管理(自动增删包的引用)

  • 注释:

    • 解释代码作用
    • 解释代码如何做的
    • 解释代码实现的原因
    • 解释代码什么情况会出错
  • 命名规范:

    • 变量: 简洁胜于冗长; 缩略词大写; 变量距离被使用的地方越远, 需要携带更多上下文信息

    • 函数: 不携带包名信息; 简短

    • 包(package):

      小写字母组成, 不包含大写字母和下划线; 简短; 不与标准库同名.

      尽量满足: 不使用常用变量名; 使用单数而不是复数; 谨慎使用缩写

  • 控制流程:

    • 避免嵌套, 保证正常流程清晰, 如去除冗余的else
    • 尽量保持正常代码路径为最小缩进, 如优先处理错误情况和特殊情况

    小结:

    • 线性原理, 逻辑走直线
    • 正常流程的代码沿着屏幕向下移动
    • 提升代码可维护性
    • 问题一般出现在复杂的条件语句和循环语句中
  • 错误和异常处理

    • 简单错误

      • 仅出现一次的错误, 其他地方不需要捕获该错误
      • 优先使用errors.New创建匿名变量来表示简单错误
      • 格式化使用fmt.Errorf
    • wrap和unwrap: error嵌套另一个error, 生成一个error的跟踪链

      fmt.Errorf("reading: %w", err)
      
    • 错误判定:

      • 使用errors.Is判断一个错误链上是否有指定错误
      • 使用errors.As获取错误链上指定种类的错误
    • panic与recover

      当程序产生panic即将崩溃, 可以使用recover函数获取这个panic并根据信息进行处理(类似其它语言的try-catch)

      • recover只能在defer中使用
      • 只在当前goroutine生效

性能优化建议

benchmark

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

go test -bench=. -benchmem

image-20230130103729294-167504625488613.png

slice

  • 预分配内存: 尽可能在使用make()初始化切片时提供容量信息

  • 切片: 本质是一个数组片段的描述, 包括数组指针, 片段长度, 片段容量

    • 切片操作不复制切片指向的元素
    • 创建一个新的切片会复用原来切片的底层数组
    • 由于底层数组在内存中有引用, 大量使用切片可能导致大内存无法释放, 可以使用copy代替切片

map

map也需要预分配内存

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

字符串操作

如字符串拼接操作, 可以使用strings.Builderbytes.Buffer代替+

  • 字符串在go中是不可变类型, 占用内存大小固定, 导致每次使用+号都会重新分配内存
  • strings.Builderbytes.Buffer是字节数组, 可以动态分配内存
  • bytes.Buffer转化为字符串时重新申请空间; strings.Builder直接将字节数组转化为字符串类型

空结构体

空结构体struct{}本身不占据内存空间, 可作为占位符使用

例如实现set, 可以考虑使用值为空结构体的map来代替

atomic包

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

性能调优实战

原则:

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

性能分析工具pprof

希望知道应用在什么地方耗费了多少cpu, memory. 使用pprof用于可视化和分析性能数据

image-20230130111031788.png

cpu采样原理

image.png

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

heap-堆内存采样原理

  • 采样程序通过内存分配器在堆上分配和释放的内存, 记录分配/释放的大小和数量
  • 采样率: 每分配512KB记录一次, 可修改
  • 采样时间: 从程序运行开始到采样时
  • 采样指标: alloc_space, alloc_objects, inuse_space, inuse_objects

Goroutine协程和ThreadCreate线程

  • Goroutine: 记录所有用户发起且在运行中的goroutine的调用栈信息
  • ThreadCreate: 记录程序创建的所有系统线程的信息

Block阻塞和Mutex锁

  • 阻塞: 采样阻塞操作的次数和耗时, 阻塞耗时超过阈值才记录
  • 锁竞争: 采样争抢锁的次数和耗时, 只记录固定比例的锁操作