Go 优化技巧

2024-07-08 18:21:50 浏览数 (2)

大概什么时候会想到优化

  • 某个函数会被频繁调用时
  • 接口或者是数据结构设计不算合理从而内存占用过高时
  • 接口响应耗时太多
  • 当代码太乱,问题频出

那发现问题的方式有哪些 ?

  • 单接口压测(这里一般是新接口或者是接口有改动时),固定QPS压测
  • 极限QPS压测(这种一般是来查看接口的最大承压规模)
  • 全链路压测(这一块儿主要是针对于一些流量高峰前的准备)

一些代码评测工具

Go代码评估工具:

  1. goreporter – 生成Go代码质量评估报告
  2. dingo-hunter – 用于在Go程序中找出deadlocks的静态分析器
  3. flen – 在Go程序包中获取函数长度信息
  4. go/ast – Package ast声明了关于Go程序包用于表示语法树的类型
  5. gocyclo – 在Go源代码中测算cyclomatic函数复杂性
  6. Go Meta Linter – 同时Go lint工具且工具的输出标准化
  7. go vet – 检测Go源代码并报告可疑的构造
  8. ineffassign – 在Go代码中检测无效赋值
  9. safesql – Golang静态分析工具,防止SQL注入

一些工具

  • benckmark
    1. 在对不同的工具、开源库等使用时,可以先对该方法进行benckmark。然后决定到底最后用哪一个
  • go pprof
    1. CPU Profile:用于诊断导致CPU占用较高的代码逻辑。
    2. Memory Profile:用于诊断导致内存占用较高的代码逻辑。
    3. Goroutine Profile:用于诊断导致Goroutine占用较高的代码逻辑,分析Goroutine执行的具体逻辑。
    4. Block Profile:用于诊断阻塞运行的代码逻辑,例如检测大锁(mutex锁定了一个运行时间较长的逻辑,并且有很多其他Goroutine在等待这个锁)

Go 内置的内存 profiler 可以让我们对线上系统进行内存使用采样,有四个相应的指标:

  1. inuse_objects:当我们认为内存中的驻留对象过多时,就会关注该指标
  2. inuse_space:当我们认为应用程序占据的 RSS 过大时,会关注该指标
  3. alloc_objects:当应用曾经发生过历史上的大量内存分配行为导致 CPU 或内存使用大幅上升时,可能关注该指标
  4. alloc_space:当应用历史上发生过内存使用大量上升时,会关注该指标

接下来会从这几方面来展开讲一下

Slice

  • 提前为slice分配内存

○ 在必要的时候,使用第三个参数: make([]T, 0, len)

○ 如果事先不知道确切的数量并且slice是临时的,可以设置得大一些,只要slice在运行时不会增长。

  • 不要忘记使用“copy”

○ 这点是需要在复制时尽可能不要使用 append,例如,在合并两个或多个slice时。

  • 不要留下未使用的slice

○ 如果需要从slice中切下一小块并仅使用它,其实主要部分也会保留下来。可以使用copy产生一个新的slice,而旧的对象让GC回收。

string

  • strings.Builder,bytes.Buffer相近
  • 优先使用 strings.Builder 而不是 =

struct

  • 通过内存对齐来减小struct大小
    • 可以对齐struct(根据字段的大小,以正确的顺序排列),从而可以减小struct本身的大小
    • 遍历 []struct{} 使用下标而不是 range
    • 有的时候我们在使用channel的时候,通常如果是来表示一个标识,而不是发送数据的情况下,用于通知子协程来完成一些任务的情况,这种情况就比较好实用 空结构体了

defer

  • 尽量不要使用 defer,或者至少 不要在循环中使用defer 。

map

  • 跟Slice一样,需要提前分配内存
    • 初始化map时,指定它的大小
  • 如果需要表示占位时,其value用空结构体表示
    • struct{} 什么都不是
  • 清空map
    • map只能增长,不能缩小。需要控制这一点——完全而明确地重置map
  • 指针使用上
    • 如果 map 不包含指针,而且要知道字符串也是指针——使用[]byte而不是字符串作为键。

interface

  • 计算内存分配 ○ 请记住,要给一个接口赋值,首先需要将其拷贝到某处,然后粘贴一个指针。关键字是拷贝。事实证明,装箱和拆箱的成本将近似于结构体的大小和一次分配。

sync.Pool

fasthttp。它几乎把所有的对象都用sync.Pool维护,所以它才自称是http的10倍,但其实没

协程池

绝大部分应用场景,go是不需要协程池的。当然,协程池还是有一些自己的优势:

  1. 可以限制goroutine数量,避免无限制的增长。
  2. 减少栈扩容的次数。
  3. 频繁创建goroutine的场景下,资源复用,节省内存。(需要一定规模。一般场景下,效果不太明显。)

go对goroutine有一定的复用能力。所以要根据场景选择是否使用协程池,不恰当的场景不仅得不到收益,反而增加系统复杂性。

减小锁消耗

并发场景下,对临界区加锁比较常见。带来的性能隐患也必须重视。常见的优化手段有:

  • 减小锁粒度:go标准库当中,math.rand就有这么一处隐患。当我们直接使用rand库生成随机数时,实际上由全局的globalRand对象负责生成。globalRand加锁后生成随机数,会导致我们在高频使用随机数的场景下效率低下。
  • atomic:适当场景下,用原子操作代替互斥锁也是一种经典的lock-free技巧。标准库中sync.map针对读操作的优化消除了rwlock,是一个标准的案例。对它的介绍文章也比较多,不在赘述。

将多个小对象合并成一个大的对象

减少不必要的指针间接引用,多使用copy引用

例如使用bytes.Buffer代替*bytes.Buffer,因为使用指针时,会分配2个对象来完成引用。

cpu耗时优化

  1. make时提前预估size
  2. 临时的map、slice采用sync.pool
  3. 大于32k也可以用sync.pool
  4. 不滥用goroutine,减少gc压力
  5. 不滥用mutex,减少上下文切换
  6. []byte与string临时变量转换用unsafe
  7. 减少reflect、defer使用
  8. atomic无锁使用

0 人点赞