高质量编程和性能调优 | 青训营笔记

91 阅读5分钟

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

课程内容

  1. 高质量编程
  • 简介
  • 编码规范
  • 性能优化建议
  1. 性能调优
  • 简介
  • 性能分析工具pprof实战
  • 性能调优案例

重点记录内容

高质量编程

目的性质

  • 简单性
  • 可读性
  • 生产力

编码规范

  • 代码格式
    使用gofmt格式化代码,或使用goimports管理包顺序以及gofmt.
    vscode有个插件可以辅助实现go的包顺序排列:

goGroupImports.jpg

  • 注释
  1. 应该解释代码作用 --注释公共符号
  2. 应该解释代码如何做的 --注释实现过程
  3. 应该解释代码实现的原因 --解释代码外部因素,提供额外上下文
  4. 应该解释代码什么情况会出错 --解释代码限制条件
    代码是最好的注释; 注释应该提供代码未表达出的上下文意思

Good code has lots of comments, bad code requires lots of comments.
好的代码有很多注释,坏代码需要很多注释
— Dave Thomas and Andrew Hunt (The Pragmatic Programmer)

  • 命名规范
  1. variable:
    ①简洁胜于冗长
    ②缩略词全大写,但其位置位于开头且不需要导出时全小写
    ③变量距离其被使用的地方越远,则需要更多的上下文信息(全局变量)

  2. fuction:
    ①命名尽量简短
    ②函数名不携带包名的上下文信息,因为包和函数总是成对出现;但可携带非此包的包名信息

  3. package
    ①只有小写字母组成,不包含大写字母和下划线,尽量使用单数而不是复数
    ②不要与标准库同名,尽量不要使用常用变量名作为包名
    ③简短并包含一定的上下文信息,尽量谨慎的使用缩写

Good naming is like a good joke. lf you have to explain it, it's not funny
好的命名就像一个好笑话。如果你必须解释它,那就不好笑了
-----Dave Cheney

  • 控制流程
  1. 线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支
  2. 正常流程代码应该是沿着屏幕向下走
  • 错误和异常处理
  1. 简单错误:
    指只出现一次,且在其他地方不需要捕获的错误,优先使用errors.New()来创建匿名变量来捕获该错误,使用fmt.Errorf()满足格式化需求
  2. 错误的Wrap和Unwrap:
    fmt.Errorf() 中使用 %w 关键字来将一个错误 wrap 至其错误链中
  3. 错误判定:
    使用 errors.Is() 可以判定错误链上的所有错误是否含有特定的错误。
    使用 errors.As() 可以在错误链上获取特定种类的错误。
  4. panic:
    不建议在业务代码中使用 panic(),如果当前 goroutine 中所有 deferred 函数都不包含 recover 就会造成整个程序崩溃,当程序启动阶段发生不可逆转的错误时,可以在 initmain 函数中使用 panic()
  5. 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)
	}
}

测试结果为:

benchmem-slice.png

  • 产生这样的区别是因为slice有预分配内存,就类似C++里面的vector一样,切片本质是一个数组片段的描述,包括了数组的指针,这个片段的长度和容量(不改变内存分配情况下的最大长度),切片操作本身不会去复制切片指向的元素,新的切片会复用原来切片的底层数组,这也是slice高效的原因之一.
  • 切片的三成员: ptr len cap , 当使用 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

benchmem-map.jpg

  • 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-xmind.jpg

这个官方案例写的很清楚 pprof实战

  • 贴一些我的运行实例图 pprof-1.jpg

pprof-3.jpg

pprof-4.png

个人总结

这节课程信息含量很大啊,从编码规范开始讲,到使用pprof工具进行性能分析,含金量很高呢.我之前的代码规范只有一个就是驼峰命名法...经过这节课的熏陶,可以试着应用到大项目中去,相信会给我们多人协作的生产有好的影响。 至于性能分析,和上节课的测试一样,都是非常重要的一个环节,后者防止出现错误,前者提高用户体验,优化服务端性能,降低成本等等,在企业中应该是非常核心的一环节~~