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

94 阅读4分钟

今天打卡第5天,主要内容为 : 高质量编程与性能调优实战

高质量编程

简介

高质量 : 编写的代码能够达到正确可靠,简洁清晰的目标可称为高质量代码

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

编程原则 :

  • 简单性 : 以简单清晰的逻辑编写代码
  • 可读性 :确保代码可读
  • 生产力 : 团队整体的工作效率非常重要

编程规范

如何编写高质量的Go代码

  • 代码格式

    1. 推荐使用gofmt自动格式化代码,go官方提供的工具,能自动格式化Go语言代码为官方统一风格

    2. goimports : gofmt + 依赖包管理

      自动增删依赖的包引用,将依赖包按字母序排序并分类

  • 注释

    注释应该解释

    • 代码作用
    • 代码如何做的
    • 代码实现的原因
    • 代码什么时候会出错
  • 命名规范

    image-20230729235340968

    image-20230729235558097

    image-20230729235720248

  • 控制流程

    1. 避免嵌套,保持正常流程清晰

      如 : 将

      if foo {
          return x
      } else {
          return nil 
      }
      

      改为 :

      if foo {
          return x
      }
      return nil 
      

      就能去掉冗余的else

    2. 保持正常代码路径为最小缩进

      • 优先处理错误情况/特殊情况,尽早返回减少嵌套。
  • 错误和异常处理

    image-20230730001325296

    image-20230730001340829

    image-20230730001416599

    image-20230730001447359

    image-20230730001522306

    image-20230730001618088

    image-20230730001701272

    小结 :

    • error尽可能提供简明的上下文信息链,方便定位问题
    • panic用于真正异常的情况
    • recover生效范围,在当前goroutine 的被defer的函数中生效

性能优化指南

性能优化建议-Benchmark

  • 性能表现需要用实际数据衡量
  • Go语言提供了支持基准性能测试的benchmark工具

语法 :

go test -bench=. -benchmem

例 :

fib.go :

package main
​
func Fib(n int) int {
    if n < 2 {
        return n
    }
    return Fib(n-1) + Fib(n-2)
}

fib_test.go

package main
​
import "testing"
​
func BenchmarkName(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Fib(10)
    }
}

运行效果 :

image-20230731223316808

性能优化建议-Slice

slice预分配内存 :

image-20230801212941563

  1. 切片本质是一个数组片段的描述 :
  • 包括数组指针
  • 片段的长度
  • 片段的容量(不改变内存分配情况下的最大长度)
  1. 切片操作并不复制切片指向的元素
  2. 创建一个新的切片会复用原来的底层数组
type slice struct {
    array unsafe.Pointer
    len int 
    cap int
}

image-20230801213722651

性能优化建议-Map

预分配 -> 性能优化

例:

func NoPreAlloc(size int) {
    data := make([]int, 0)
    for k := 0; k < size; k++ {
        data = append(data, k)
    }
}
​

改为 :

func NoPreAlloc(size int) {
    data := make([]int, 0, size)
    for k := 0; k < size; k++ {
        data = append(data, k)
    }
}

Map预分配内存

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

性能优化建议 - 字符串处理

  • 常见的字符串拼接方式

    1.+=

    func Plus(n int,str string) string {
        s := ""
        for i:=0; i<n;i++ {
            s += str
        }
        return s
    }
    

    2.使用strings.Builder

    func StrBuilder(n int, str string) string {
        var builder strings.Builder
        for i := 0; i < n; i++ {
            builder.WriteString(str)
        }
        return builder.String()
    }
    

    3.使用strings.Builder

    func ByteBuffer(n int,str string) string {
        buf := new(bytes.Buffer)
        for i:=0;i<n;i++ {
            buf.WriteString(str)
        }
        return buf.String()
    }
    
  • 使用+拼接的性能最差,string.Builder,bytes.Buffer相近,string.Buffer更快

  • 分析 :

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

使用strings.Builder的效率是最快的,实现内存预分配能再加快效率。

性能优化建议-空结构体

  • 使用空结构体能节省空间,空结构体struct{}实例不占用任何的内存空间

  • 课作为各种场景下的占位符使用

    • 节省资源
    • 空结构体本身具有很强的语义,即这里不需要任何值,仅作为占位符
func EmptyStructMap(n int) {
    m := make(map[int]struct{})
​
    for i := 0; i < n; i++ {
        m[i] = struct{}{}
    }
}
​
func BoolMap(n int) {
    m := make(map[int]bool)
    
    for i:=0;i<n;i++ {
        m[i] = false
    }
}

使用场景 :

  • 实现Set,可以考虑用map来替代。只使用Map中的键。
  • 对于这个场景,只使用Map中的键,而不需要用到值
  • 即便是map的值设置成bool类型,也会多占用一个空间
  • 一个开源实现 : github.com/deckarep/go…

性能优化建议 - atomic包

如何使用atomic包 :

image-20230803153358748

性能优化建议 -- 总结

  • 避免常见的性能陷阱可以保证大部分程序的性能
  • 普遍应用代码,不要一味的追求程序的性能
  • 越高级的性能优化手段越容易出现问题
  • 再满足正确可靠,简洁清晰的质量要求下提高程序性能。

性能调优分析工具

性能调优原则 :

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

工具pprof

  • 希望知道应用再什么地方消耗量多少的CPU,Memory
  • pprof是用于可视化和分析性能分析数据的工具

功能简介

image-20230803154420646

实战

搭建pprof实践项目

  • Github(来自Wolfogre)
  • 地址 : github.com/wolfogre/go…
  • 项目提前埋入一些炸弹代码,产生可观测的性能问题

前置准备 :

  • 下载项目代码,能够编译运行
  • 会占用1CPU核心和超过1GB的内存

运行效果(不断打印日志) :

image-20230803155353762

保持程序运行,打开浏览器访问 http://localhost:6060/debug/pprof/,可以看到如下页面:

image-20230803155511490

任务管理器窗口 :

image-20230803155725325

使用 go tool pprof

go tool pprof http://localhost:6060/debug/pprof/profile

image-20230803160201237

输入 top 命令,查看 CPU 占用较高的调用:

image-20230803161704338

  • flat : 当前函数本身的执行耗时
  • flat% : flat占CPU总时间的比例
  • sum% : 上面每一行的flat%总和
  • cum : 指当前函数本身加上其调用函数的总耗时
  • cum% : cum占CPU总时间的比例
  • Flat == Cum : 函数中没有调用其它函数
  • Flat == 0 : 函数中只有其它函数的调用

输入list Eat命令 ,查看问题具体在代码的哪一个位置::

image-20230803161737902

  • 命令 : web
  • 调用关系可视化

image-20230803162757704