go 高质量编码规范与性能调优 | 青训营笔记

102 阅读5分钟

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

day3 高质量编码规范与调优

昨天学go-zero学到昏死了,忘记写笔记了。

高质量编程

边界条件是否考虑完备

异常情况处理,稳定性保证

易读易维护

注释应该解释代码的作用

代码实现的原因

代码什么情况下会出错

(未表达出的上下文信息)

性能优化

评估代码性能:

Benchmark

go自身有Benchmark命令来进行性能测试。

基准测试benchmark是go testing库提供的,用来度量程序性能,算法优劣的工具。

指定一个时间(默认是1秒),看测试对象在达到时间上限时,最多能被执行多少次和在此期间测试对象内存分配情况。

如一个斐波那契数列的函数Fib及其基准测试函数如下:

package start

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

import "testing"

// BenchmarkFib10 run 'go test -bench=. -benchmem' to get the benchmark result
func BenchmarkFib10(b *testing.B) {
    // run the Fib function b.N times
    for n := 0; n < b.N; n++ {
       Fib(10)
   }
}

进入到文件目录,运行

go test -bench=BenchmarkFib10 -benchmem

表示运行测试BenchmarkFib10,并显示内存

GOMAXPROCS表示逻辑CPU数量。

要求

基准测试的代码文件必须以_test.go结尾

基准测试的函数必须以Benchmark开头

基准测试函数必须接受一个指向Benchmark类型的指针作为唯一参数(b *testing.B)

基准测试函数不能有返回值

b.ResetTimer是重置计时器,这样可以避免for循环之前的初始化代码的干扰

最后的for循环很重要,被测试的代码要放到循环里

b.N是基准测试框架提供的,表示循环的次数

常用API

b.StopTimer()

b.StartTimer()

b.ResetTimer()

b.Run(name string, f func(b *B))

b.RunParallel(body func(*PB))

b.ReportAllocs()

b.SetParallelism(p int)

b.SetBytes(n int64)

testing.Benchmark(f func(b *B)) BenchmarkResult

操作命令

go test -bench=BenchmarkFoo

go test -bench=.

// 加上 -bench= 测试名字, .表示运行所有的基准测试,名字可用正则。

go test -bench=BenchmarkFoo -benchtime=5s/10000x

// 加上 -benchtime 设置时间,s表示秒,x表示执行次数

go test -bench=BenchmarkFoo -benchtime=5s -count=3

// 加上 -count 表示几次测试

go test -bench=. -benchmem

// 加上 -benchmem 查看内存

go test -bench=. -benchmem -cpuprofile profile.out

go test -bench=. -benchmem -memprofile memprofile.out

go tool pprof profile.out

go tool pprof memprofile.out

// 结合 pprof 输出查看 cpu和内存。

性能优化建议

Slice

slice 预分配内存。

尽可能在使用make()初始化切片时提供容量信息。(cap)

make([]int,0,size) 最好传入size参数

为什么?

提前分配容量,直接指定了容量大小,只有一次分配。因为Silce本质上是底层数组+指针。

切片包括:数组指针,片段长度,片段容量

长度表示左指针至右指针之间的距离,容量表示左指针至底层数组末尾的距离。

切片的扩容机制,append的时候,如果长度增加后超过容量,则将容量增加2倍,同时变换了底层数组。

选取合适的容量,避免了额外的扩容操作。

大内存未释放。

切片操作实际上并不复制切片指向的元素,

所以对一个切片创建一个新切片时,会复用原切片的底层数组。如果原切片比较大,创建的新切片只取了几个元素,这时就会导致原来的底层数组在内存中依然会有引用,得不到释放。

可以使用copy代替re-slice

Map

map预分配内存(make)

字符串处理

使用strings.Builder 拼接字符串

使用+拼接性能最差,strings.Builder,bytes.Buffer 差不多,strings.Buffer更快

为什么要用strings.Builder?

字符串在go中是不可变类型,占用的内存大小固定。

使用+,每次都会重新分配内存。

strings.Builder,bytes.Buffer 底层都是[]byte 数组

内存扩容策略,不需要每次拼接重新分配内存。

而strings.Builder与bytes.Buffer相比,后者转换为字符串时重新申请了一块空间。

前者直接将底层的[]byte转换成了字符串类型返回。

在已知字符串长度时,进一步提升处理性能:

字符串拼接其实和slice差不多,也有相关的支持预分配的方法。

已知字符串长度时,使用builder.Grouw(n * len(str)) 或者buffer中的

buf.Grow(n * len(str))

空结构体

使用空结构体节省内存空间。(空结构体不占用任何内存空间)

可作为各种场景下的占位符使用:

定义map时使用空结构体组为value m := make(map[int]struct{})

空结构体本身具有很强的语义,即这里不需要任何值,仅仅作为占位符。

struct {} {}是一个复合字面量,它构造了一个struct {}类型的值,该值也是空。

可以考虑实现set时(不重复的无序 集合),用map来代替。因为set本身就是一个没有value的map。只需要用到map的键。

即使将map的值设置为bool,也会多占据1个字节空间。

atomic包

在多线程下,使用atomic包的性能要比加锁好

锁的实现是通过操作系统来实现的,属于系统调用

atomic操作是通过硬件实现,效率比锁高

sync.Mutex应该用来保护一段逻辑,不仅仅用于保护一个变量。

对于非数值操作,可以使用atomic.Value,能承载一个interface{}

性能调优原则

依靠数据而不是猜测

定位最大瓶颈而不是细枝末节

不要过早优化,不要过度优化