性能调优
# 性能调优
# 编码规范
什么是高质量 —— 编写的代码能的够达到正确可靠、简洁清晰的目标可称之为高质量代码
- 各种边界条件是否考虑完备
- 异常情况处理,稳定性保证
- 易读易维护
编程原则
实际应用场景干变万化,各种语言的特性和语法各不相同,但是高质量编程遵循的原则是相通的
简单性
- 消除多余的复杂性,以简单清晰的逻辑编写代码
- 不理解的代码无法修复改进
可读性
- 代码是写给人看的,而不是机器
- 编写可维护代码的第一步是确保代码可读
生产力
- 团队整体工作效率非常重要
如何编写高质量的 G0 代码
- 代码格式
- 注释
- 命名规范
- 控制流程
- 错误和异常处理
# 代码格式
- gofmt:Go 语言官方提供的工具,能自动格式化 Go 语言代码为官方统一风格常见 IDE 都支持方便的配置
- goimports:Go 语言官方提供的工具,实际等于 gofmt 加上依赖包管理,自动增删依赖的包引用、将依赖包按字母序排序并分类
# 注释
# 注释应该解释代码作用
适合注释公共符号
https://github.com/golang/go/blob/master/src/os/file.go#L313


# 注释应该解释代码如何做的
适合注释方法
https://github.com/golang/go/blob/master/src/net/http/client.go#L678


# 注释应该解释代码实现的原因
解释代码的外部因素,提供额外上下文
https://github.com/golang/go/blob/master/src/net/http/client.go#L521

# 注释应该解释代码什么情况会出错
适合解释代码的限制条件
https://github.com/golang/go/blob/master/src/time/format.go#L1344

# 公共符号始终要注释
- 包中声明的每个公共的符号:变量、常量、函数以及结构都需要添加注释
- 任何既不明显也不简短的公共功能必须予以注释
- 无论长度或复杂程度如何对库中的任何函数都必须进行注释
https://github.com/golang/go/blob/master/src/io/io.go#L638

有一个例外,不需要注释实现接口的方法。下图图里的注释没有提供有用的信息。它没有告诉你这个方法做了什么,更糟糕是它告诉你去看其他地方的文档在这种情况下,建议完全删除该注释。

最后选取一个 go 仓库中相对完整的代码块来说明注释
https://github.com/golang/go/blob/master/src/io/io.go#L455
首先 LimitReader 的功能有注释说明,然后是 LimitedReader 结构体的说明,就在使用它的函数之前 LimitedReader.Read 的声明遵循 LimitedReader 本身的声明,里面已经有详细说明,所以没有注释

- 代码是最好的注释
- 注释应该提供代码未表达出的上下文信息
# 命名规范
# variable
- 简洁胜于冗长
- 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
- 例如使用 ServeHTTP 而不是 ServeHttp
- 使用 XMLHTTPRequest 或者 xmlHTTPRequest
- 变量距离其被使用的地方越远,则需要携带越多的上下文信息
- 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义

i 和 index 的作用域范围仅限于 for 循环内部时,index 的额外冗长几乎没有增加对于程序的理解

- 将 deadli ne 替换成 t 降低了变量名的信息量
- t 常代指任意时间
- deadline 指截止时间,有特定的含义
# function
- 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的
- 函数名尽量简短
- 当名为 foo 的包某个函数返回类型 Foo 时,可以省略类型信息而不导致歧义
- 当名为 foo 的包某个函数返回类型 T 时(T 并不是 Foo),可以在函数名中加入类型信息

在调用 http 包的 Server 方法时,代码是 http.Server, 携带有 http 包名,所以函数名中无需添加包信息
# package
- 只由小写字母组成。不包含大写字母和下划线等字符
- 简短并包含一定的上下文信息。例如 schema、task 等
- 不要与标准库同名。例如不要使用 sync 或者 strings
以下规则尽量满足,以标准库包,名为例
- 不使用常用变量名作为包名。例如使用 bufio 而不是 buf
- 使用单数而不是复数。例如使用 encoding 而不是 encodings
- 谨慎地使用缩写。例如使用 ft 在不破坏上下文的情况下比 format 更加简短
# 总结
关于命名的大多数规范核心在于考虑上下文
人们在阅读理解代码的时候也可以看成是计算机运行程序,好的命名能让人把关注点留在主流程上,清晰地理解程序的功能,避免频繁切换到分支细节,增加理解成本
# 控制流程
- 避免嵌套,保持正常流程清晰 如果两个分支中都包含 return 语句,则可以去除冗余的 else

- 尽量保持正常代码路径为最小缩进,优先处理错误情况 / 特殊情况,并尽早返回或继续循环来减少嵌套,增加可读性


https://github.com/golang/go/blob/master/src/bufio/bufio.go#L277

线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支
正常流程代码沿着屏幕向下移动
提高代码的可读性
故障问题大多出现在复杂的条件语句和循环语句中
# 错误和异常处理
# 简单错误处理
- 简单的错误指的是仅出现一次的错误,且在其他地方不需要捕获该错误
- 优先使用 errors.New 来创建匿名变量来直接表示简单错误
- 如果有格式化的需求,使用 fmt.Errorf
https://github.com/golang/go/blob/master/src/net/http/client.go#L802

# 错误的 Wrap 和 Unwrap
- 错误的 Wrap 实际上是提供了一个 error 嵌套另一个 error 的能力,从而生成一个 error 的跟踪链
- 在 fmt.Errorf 中使用:% w 关键字来将一个错误关联至错误链中
Go1.13 在 errors 中新增了三个新 API 和一个新的 format 关键字,分别是 errors.Is、errors.As 、errors.Unwrap 以及 fmt.Errorf 的 % w。如果项目运行在小于 Go1.13 的版本中,导入 golang.org/x/xerrors 来使用。以下语法均已 Go1.13 作为标准。
https://github.com/golang/go/blob/master/src/cmd/go/internal/work/exec.go#L983

# 错误判定
- 使用 errors.Is 可以判定错误链上的所有错误是否含有特定的错误。
- 不同于使用 ==,使用该方法可以判定错误链上的所有错误是否含有特定的错误
https://github.com/golang/go/blob/master/src/cmd/go/internal/modfetch/sumdb.go#L208

- 在错误链上获取特定种类的错误,使用 errors.As
https://github.com/golang/go/blob/master/src/errors/wrap_test.go#L255

# panic
- 不建议在业务代码中使用 panic
- 如果当前 goroutine 中所有 deferred 函数都不包含 recover 就会造成整个程序崩溃
- 若问题可以被屏蔽或解决,建议使用 error 代替 panic
- 当程序启动阶段发生不可逆转的错误时,可以在 init 或 main 函数中使用 panic
https://github.com/Shopify/sarama/blob/main/examples/consumergroup/main.go#L94

# recover
- recover 只能在被 defer 的函数中使用
- 嵌套无法生效
- 只在当前 goroutine 生效
- defer 的语句是后进先出
https://github.com/golang/go/blob/master/src/fmt/scan.go#L247

- 如果需要更多的上下文信息,可以 recover 后在 log 中记录当前的调用栈。
https://github.com/golang/website/blob/master/internal/gitfs/fs.go#L228

# 总结
panic 用于真正异常的情况
error 尽可能提供简明的上下文信息,方便定位问题
recover 生效范围,在当前 goroutine 的被 defer 的函数中生效
# 性能优化建议
- 在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的效率
- 性能优化是综合评估,有时候时间效率和空间效率可能对立
- 针对 Go 语言特性,介绍 Go 相关的性能优化建议
# Benchmark
Go 语言提供了支持基准性能测试的 benchmark 工具
go test -bench=. -benchmem

# Slice 预分配内存
- 在尽可能的情况下,在使用 make () 初始化切片时提供容量信息,特别是在追加切片时

- 切片本质是一个数组片段的描述,包括了数组的指针,这个片段的长度和容量 (不改变内存分配情况下的最大长度)
- 切片操作并不复制切片指向的元素,创建一个新的切片会复用原来切片的底层数组,因此切片操作是非常高效的
- 切片有三个属性,指针 (ptr)、长度 (len) 和容量 (cap)。append 时有两种场景:
- 当 append 之后的长度小于等于 cap,将会直接利用原底层数组剩余的空间
- 当 append 后的长度大于 cap 时,则会分配一块更大的区域来容纳新的底层数组
- 因此,为了避免内存发生拷贝,如果能够知道最终的切片的大小,预先设置 cap 的值能够获得最好的性能

另一个陷阱:大内存得不到释放
- 在已有切片的基础上进行切片,不会创建新的底层数组。因为原来的底层数组没有发生变化,内存会一直占用,直到没有变量引用该数组
- 因此很可能出现这么一种情况,原切片由大量的元素构成,但是我们在原切片的基础上切片,虽然只使用了很小一段,但底层数组在内存中仍然占据了大量空间,得不到释放
- 推荐的做法,使用 copy 替代 re-slice

go test -run=. -v

# Map 预分配内存

- 不断向 map 中添加元素的操作会触发 map 的扩容
- 提前分配好空间可以减少内存拷贝和 Rehash 的消耗
- 根据实际需求提前预估好需要的空间
# 使用 strings.Builder
常见的字符串拼接方式


- 使用 + 拼接性能最差,strings.Builder,bytes.Buffer 相近,strings.Buffer 更快
原理
- 字符串在 Go 语言中是不可变类型,占用内存大小是固定的,当使用 + 拼接 2 个字符串时,生成一个新的字符串,那么就需要开辟一段新的空间,新空间的大小是原来两个字符串的大小之和
- strings.Builder,bytes.Buffer 的内存是以倍数申请的
- strings.Builder 和 bytes.Buffer 底层都是 [] byte 数组,bytes.Buffer 转化为字符串时重新申请了一块空间,存放生成的字符串变量,而 strings.Builder 直接将底层的 [] byte 转换成了字符串类型返回

使用 Grow 方法预分配内存

# 使用空结构体节省内存
- 空结构体不占据内存空间,可作为占位符使用
- 节省资源,空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符


- Go 语言标准库没有提供 Set 的实现,通常使用 map 来代替。
- 对于集合场景,只需要用到 map 的键而不需要值
- 即使是将 map 的值设置为 bool 类型,也会多占据 1 个字节空间
一个开源实现:https://github.com/deckarep/golang-set/blob/main/threadunsafe.go
# 使用 atomic 包

- 锁的实现是通过操作系统来实现,属于系统调用
- atomic 操作是通过硬件实现的,效率比锁高很多
- sync.Mutex 应该用来保护一段逻辑,不仅仅用于保护一个变量
- 对于非数值系列,可以使用 atomic.Value,atomic.Value 能承载一个 interface {}
# 总结
避免常见的性能陷阱可以保证大部分程序的性能
针对普通应用代码,不要一味地追求程序的性能,应当在满足正确可靠、简洁清晰等质量要求的前提下提高程序性能
越高级的性能优化手段越容易出现问题
# 性能调用实战
性能调优原则
- 要依靠数据不是猜测
- 要定位最大瓶颈而不是细枝末节
- 不要过早优化
- 不要过度优化
# 性能分析工具 pprof
- pprof 是用于可视化和分析性能分析数据的工具
- 可以知道应用在什么地方耗费了多少 CPU、memory 等运行指标

https://github.com/wolfogre/go-pprof-practice