这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天
课程内容
- 高质量编程
- 简介
- 编码规范
- 性能优化建议
- 性能调优
- 简介
- 性能分析工具pprof实战
- 性能调优案例
重点记录内容
高质量编程
目的性质
- 简单性
- 可读性
- 生产力
编码规范
- 代码格式
使用gofmt格式化代码,或使用goimports管理包顺序以及gofmt.
vscode有个插件可以辅助实现go的包顺序排列:
- 注释
- 应该解释代码作用 --注释公共符号
- 应该解释代码如何做的 --注释实现过程
- 应该解释代码实现的原因 --解释代码外部因素,提供额外上下文
- 应该解释代码什么情况会出错 --解释代码限制条件
代码是最好的注释; 注释应该提供代码未表达出的上下文意思
Good code has lots of comments, bad code requires lots of comments.
好的代码有很多注释,坏代码需要很多注释
— Dave Thomas and Andrew Hunt (The Pragmatic Programmer)
- 命名规范
-
variable:
①简洁胜于冗长
②缩略词全大写,但其位置位于开头且不需要导出时全小写
③变量距离其被使用的地方越远,则需要更多的上下文信息(全局变量) -
fuction:
①命名尽量简短
②函数名不携带包名的上下文信息,因为包和函数总是成对出现;但可携带非此包的包名信息 -
package
①只有小写字母组成,不包含大写字母和下划线,尽量使用单数而不是复数
②不要与标准库同名,尽量不要使用常用变量名作为包名
③简短并包含一定的上下文信息,尽量谨慎的使用缩写
Good naming is like a good joke. lf you have to explain it, it's not funny
好的命名就像一个好笑话。如果你必须解释它,那就不好笑了
-----Dave Cheney
- 控制流程
- 线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支
- 正常流程代码应该是沿着屏幕向下走
- 错误和异常处理
- 简单错误:
指只出现一次,且在其他地方不需要捕获的错误,优先使用errors.New()来创建匿名变量来捕获该错误,使用fmt.Errorf()满足格式化需求 - 错误的Wrap和Unwrap:
在fmt.Errorf()中使用%w关键字来将一个错误 wrap 至其错误链中 - 错误判定:
使用errors.Is()可以判定错误链上的所有错误是否含有特定的错误。
使用errors.As()可以在错误链上获取特定种类的错误。 - panic:
不建议在业务代码中使用panic(),如果当前goroutine中所有deferred函数都不包含recover就会造成整个程序崩溃,当程序启动阶段发生不可逆转的错误时,可以在init或main函数中使用panic() - recover():
recover()只能在被defer(在其之后的语句会在return后才执行,后进先出)的函数中使用,嵌套无法生效,只在当前goroutine生效,让因为panic陷入宕机的goroutine恢复,并读取到panic的输入,如果需要更多的上下文信息,可以recover()后在log中记录当前的调用栈.
PS: 其实感觉 panic + recover 类似于其他语言的 try/catch 机制,erro负责提供上下文来定位问题位置,panic是真正出错误的时候调用,recover则是用来对错误进行一些操作
性能优化建议
在slice.go文件中有:
func NoPreAlloc(size int) {
data := make([]int, 0)
for k := 0; k < size; k++ {
data = append(data, k)
}
}
func PreAlloc(size int) {
data := make([]int, 0, size)
for k := 0; k < size; k++ {
data = append(data, k)
}
}
slice_test.go文件中有:
func BenchmarkNoPreAlloc(b *testing.B) {
for n := 0; n < b.N; n++ {
NoPreAlloc(100)
}
}
func BenchmarkPreAlloc(b *testing.B) {
for n := 0; n < b.N; n++ {
PreAlloc(100)
}
}
测试结果为:
- 产生这样的区别是因为slice有预分配内存,就类似C++里面的vector一样,切片本质是一个数组片段的描述,包括了数组的指针,这个片段的长度和容量(不改变内存分配情况下的最大长度),切片操作本身不会去复制切片指向的元素,新的切片会复用原来切片的底层数组,这也是slice高效的原因之一.
- 切片的三成员:
ptrlencap, 当使用append()时,若没达到cap 则直接利用原底层数组的剩余空间,否则会分配一块更大的区域,把底层数组搬进去再将append()的元素加入,因此为了避免频繁的内存拷贝,一开始最好就设置一个长度合适的cap值,就如图中所示,能获得更好的性能. - 另外存在一个问题--大内存无法释放:就是如果在已有切片的基础上进行切片操作,就会如上面所说一直使用那个底层数组,内存会一直被占用,直到无变量引用它.推荐使用
copy()函数替代re-silce操作.
在map.go文件中有:
package benchmap
func NoPreAlloc(size int) {
data := make(map[int]int)
for i := 0; i < size; i++ {
data[i] = 1
}
}
func PreAlloc(size int) {
data := make(map[int]int, size)
for i := 0; i < size; i++ {
data[i] = 1
}
}
在map_test.go文件中有:
package benchmap
import "testing"
func BenchmarkNoPreAlloc(b *testing.B) {
for n := 0; n < b.N; n++ {
NoPreAlloc(1000)
}
}
func BenchmarkPreAlloc(b *testing.B) {
for n := 0; n < b.N; n++ {
PreAlloc(1000)
}
}
使用 go test -bench=. -benchmem
- 和
slice原因差不多:不断向 map 中添加元素的操作会触发 map 的扩容 - 可以根据实际需求提前预估好需要的空间,提前分配好空间可以减少内存拷贝和 Rehash 的消耗
字符串拼接中效率排行: string.Build() > bytes.Buffer > +
- 字符串在 Go 语言中是不可变类型,占用内存大小是固定的,当使用 + 拼接 2 个字符串时,生成一个新的字符串,那么就需要开辟一段新的空间,新空间的大小是原来两个字符串的大小之和
- 而前面两个内存是以倍数申请的,不用像最后一个那样每次拼接就申请;第一个之所以更快是因为其直接将底层的
[]byte转换成字符串类型返回,而第二个还需要重新申请一块空间存放生成的字符串变量.
使用空结构体节省内存: 其实就是在实现Set的时候 可以用 map[int]struct{} 代替 map[int]bool ,空结构体不占据内存空间,可作为占位符使用
在想实现互斥信号量的时候,可以使用 atomic 来替代锁,其是通过硬件实现,比系统调用的锁效率更高; sync.Mutex 应该来保护一段逻辑,而不是仅仅加锁在一个变量上;对于非数值操作,可以使用 atomic.Value
能承载一个 interface{}
性能调优
原则
- 依靠数据而非猜测,事实说话!
- 定位最大瓶颈而不是细枝末节,因为优化是有成本的,尽量达到能效比最大化.
- 不要过早或过度优化,前者是因为代码可能存在很多变动,后者是为了后期维护的稳定性.
性能分析工具 - pprof
这个官方案例写的很清楚 pprof实战
- 贴一些我的运行实例图
个人总结
这节课程信息含量很大啊,从编码规范开始讲,到使用pprof工具进行性能分析,含金量很高呢.我之前的代码规范只有一个就是驼峰命名法...经过这节课的熏陶,可以试着应用到大项目中去,相信会给我们多人协作的生产有好的影响。 至于性能分析,和上节课的测试一样,都是非常重要的一个环节,后者防止出现错误,前者提高用户体验,优化服务端性能,降低成本等等,在企业中应该是非常核心的一环节~~