Go编码规范和性能优化 | 青训营笔记

483 阅读4分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第4篇笔记

GO-学习笔记

编码规范

  • 简洁、清晰、稳定
  • 简单性、可读性、生产力
  • 有效注释,解释代码功能,提供额外的上下文
  • 代码格式
  1. gofmt,格式化工具
  2. goimports增删依赖包
  • 命名规范
  1. 缩略词全部大写
  2. 重要变量名称携带上下文信息
  3. 函数方法命名不与包名重复,避免冗余
  4. 包名只用小写,不与标准库冲突,不用变量名和复数
  • 控制流程,避免嵌套,去掉冗余的else
  • 错误提示,表明原因
  1. errors.New()
  2. fmt.Errorf("%w", err)嵌套错误输出
  3. 业务代码不使用panic,用于真正异常的情况
  4. recover机制,在defer函数中使用,避免嵌套,打印错误上下文

性能调优

  • 避免常见性能陷阱

性能基准测试

  • benchmark工具

go test -bench=. -benchmem

  • 方法/执行次数/每次执行时间/每次执行申请内存大小/每次执行申请几次内存 image.png

集合指定容量

  • slice和map使用时,提前给一个容量
func NoPreAlloc(size int) {
   data := make([]int, 0)
   for i := 0; i < size; i++ {
      data = append(data, i)
   }
}

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

字符串拼接测试

  • go中字符串是不可变,直接string拼接,都会创建一个新的字符串;strings.Builder和bytes.Buffer底层维护了bytes数组,内部维护了扩容的策略,不用每次都创建
func Plus(n int, str string) string {
   s := ""
   for i := 0; i < n; i++ {
      s += str
   }
   return s
}

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

func ByteBuffer(n int, str string) string {
   buf := new(bytes.Buffer)
   for i := 0; i < n; i++ {
      buf.WriteString(str)
   }
   return buf.String()
}

image.png

  • buffer转字符串,申请了空间
func (b *Buffer) String() string {
   if b == nil {
      // Special case, useful in debugging.
      return "<nil>"
   }
   return string(b.buf[b.off:])
}
  • builder转字符串,直接将数组转化为string
func (b *Builder) String() string {
   return *(*string)(unsafe.Pointer(&b.buf))
}

Grow预分配大小

  • 预分配效率更高
func PreStrBuilder(n int, str string) string {
   var builder strings.Builder
   builder.Grow(n * len(str))
   for i := 0; i < n; i++ {
      builder.WriteString(str)
   }
   return builder.String()
}

func PreByteBuffer(n int, str string) string {
   buf := new(bytes.Buffer)
   buf.Grow(n * len(str))
   for i := 0; i < n; i++ {
      buf.WriteString(str)
   }
   return buf.String()
}

image.png

空结构体

  • 不占内存空间,仅作为占位符,增强语义
  • 结合map,利用结构体,实现set

atomic

  • 多线程下避免使用sync.Mutex上锁,属于系统调用
  • atomic维护变量
type atomicCounter struct {
   i int32
}
func AtomicAddOne(c *atomicCounter) {
   atomic.AddInt32(&c.i, 1)
}

性能调优实战

  • 定位最大的瓶颈
  • 定位什么地方耗费了多少CPU和内存

PProf

  • 默认端口6060 image.png

CPU

  • 采集10秒的运行结果,并进行pprof控制台查看

go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10" image.png

  • top查看CPU占比较多的函数
  1. flat函数执行的时间,flat == 0当前函数只调用其他函数
  2. flat%占CPU总时间的比例
  3. sum%向上累加
  4. cum 当前函数及调用函数的总耗时,flag == cum当前函数没有调用其他函数
  5. cum%占CPU总时间的比例 image.png
  • list Eat根据正则表达式查看某个函数的执行情况 image.png
  • web以图的形式展示 image.png

原理

  • 采样对象:函数调用和占用的时间
  • 采样率:100次/秒
  • 采样时间:从手动启动到结束
  • 开始采样时,设置信号处理函数,开启定时器;停止采样时,取消信号处理函数,关闭定时器 image.png

内存

  • 查看内存情况并以网页的形式展开

go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap" image.png

  • top形式 image.png
  • source形式,类似list image.png
  • 采样类型
  1. alloc_objects累计申请对象数
  2. alloc_space累计申请空间大小
  3. inuse_objects当前持有对象数
  4. inuse_space当前占用内存大小 image.png

原理

  • 通过内存分配器在堆上分配和释放的内存,记录分配/释放的大小和数量
  • 采样率:每分配512KB记录一次
  • 采样时间:从开始到结束
  • 计算方式:inuse = alloc - free

协程

  • 记录所有用户发起且运行中的堆栈调用信息
  • go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
  • flame graph火焰图 image.png

原理

  • STW,遍历allg切片,输出创建goroutine的堆栈

线程

  • 记录程序创建的所有系统线程的信息
  • go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/threadcreate"

原理

  • STW,遍历allm链表,输出创建线程的堆栈

锁操作

  • go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex" image.png

原理

  • 采样争抢锁的次数和耗时
  • 采样率:只记录固定比例的锁操作
  • 锁竞争时,上报调用栈和消耗时间,是否在当前比例之内

阻塞

  • go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block" image.png

原理

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

案例

  • 使用库的规范
  • 高并发场景的检测
  • 正确性是基础,记录请求和响应,保证幂等性
  • 上线后,逐步放量,持续收集性能数据
  • go语言的本身优化,运行时参数,代码编译流程

评估手段

  • 性能评估,不同负载下的性能表现
  • 请求流量构造,模仿线上真实情况
  • 压测范围,单机和集群压测
  • 性能数据采集,单机和集群性能数据