Go语言高性能编程 | 青训营笔记

61 阅读3分钟

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

高效代码编程

  • 高质量代码定义:正确可靠,简单清晰

  • 高质量编程原则:简单性、可读性、生产力

  • 官方格式化工具:gofmt,goimports(实际上等于gofmt+依赖包管理)

  • defer语句是在函数返回前调用,多个defer语句遵循后进先出的顺序

  • 注释相关

    • 代码是最好的注释
    • 注释应该提供代码未表达的上下文信息
  • 变量命名

    • 简洁胜于冗长
    • 缩略词全大写(但当其开头时不大写)
    • 用更长的变量名是真的有必要使用,否则简洁
    • 传参的名最好含有参数本身信息
  • 函数命名

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

    • 只由小写字母组成。不包含大写字母和下划线等字符
    • 简短并包含一定的上下文信息。例如 schema、task 等不要与标准库同名。例如不要使用 sync 或者 strings 以下规则尽量满足,以标准库包名为例
    • 不使用常用变量名作为包名。例如使用 bufio 而不是 buf使用单数而不是复数。例如使用 encoding 而不是 encodings谨慎地使用缩写。例如使用 fmt 在不破坏上下文的情况下比 format 更加简短
  • 编码规范

    • 避免if语句嵌套,能单独提出来的if就不要嵌套,能不写else就不写

高效性能优化

  • 官方提供benchmark用于测试函数的性能

  • 优化条例

    • slice使用时指定大小(本质是一个数组的描述),扩容过程中会复用原来的slice(注意和java区分),指定大小避免扩容消耗

    • 由于slice扩容会复用,所以在使用大切片时,如果只需要获取其中的一小部分,那么应该用定义一个新的切片然后copy

    • 同理map也需要预分配,具体原因如下

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

      • 使用 strings.Builder使用 + 拼接性能最差,strings.Builder,bytes.Buffer 相近,strings.Buffer 更快
      • 字符串在 Go 语言中是不可变类型,占用内存大小是固定的,使用 + 每次都会重新分配内存
      • strings.Builder,bytes.Buffer 底层都是 byte 数组,内存扩容策略不需要每次拼接重新分配内存
      • 如果知道字符串长度的时候可以使用grow函数来指定字符串builder或者buffer的长度,达到最优化
    • 空结构体由于不消耗内存,可以在使用时节省内存消耗

      • 实现 Set,可以考虑用 map 来代替,对于这个场景,只需要用到 map 的键,而不需要值,所以可以直接使用空结构体,即使是将 map 的值设置为 bool 类型,也会多占据 1 个字节空间
    • atomic包相关

      • atomic包(硬件实现)在实现同步时比锁(线程状态切换)的开销小多了
//bad,消耗100MB
func GetLastBySlice(origin []int) []int {
    return origin[len(origin)-2:]
}
//good,消耗3MB
func GetLastByCopy(origin []int) []int {
    result := make([]int, 2)
    copy(result, origin[len(origin)-2:])
    return result
}

性能调优实战

  • pprof:pprof 是用于可视化和分析性能分析数据的工具