高质量编程以及性能优化入门
这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天。收获颇丰嘿嘿!
高质量编程
如何编写高质量的Go代码
- 注释:
- 合理的注释公共符号(比如对外提供的函数,利用注释描述他的功能)
- 对代码中复杂的,并不明显的逻辑进行说明
- 解释代码的外部因素,这些因素脱离上下文以后很难理解
- 提醒使用者一些潜在的限制条件或者会无法处理的情况:比如是否有安全隐患,输入限制
- 总结:代码是最好的注释,注释应该提示出代码没有表达出的上下文信息
- 命名规范
- 函数:
-
函数名不要携带包名的上下文信息,因为包名和函数名总是成对出现的
举个栗子:http包中创建服务的函数命名成 Serve比ServeHTTp好,因为在实际在调用http包Serve方法的时候,写的代码http.Serve,会携带有http包名,所以函数名字就不需要再包含http了。
-
当名为foo包的某个函数返回Foo时,可以省略类型信息而不导致歧义
-
当名为foo包的某个函数返回其他类型(不是Foo)的时候可以在函数命名中加入类型信息
(后面两条有待理解 T.T)
-
- 包:
- 由小写字母组成,不包含大写字母和下划线
- 不要与标准库同名
- 函数:
- 编码规范
- 控制流程
-
避免嵌套,保持正常流程清晰。比如if语句里两个都包含return,可以除去冗余的else
-
优先处理错误情况,尽早返回或继续循环来减少嵌套 (代码解释见下)
正常流程的代码被嵌在两个if里面,这样有很多弊端:1.很难发现成功退出的条件 2.函数如果返回一个错误,要追溯到匹配的左括号,才能了解什么时候触发的错误 3. 正常流程还要增加一步,调用新的函数,那就又要新增一层嵌套
-
所以我们写的时候要遵循线性原理,逻辑尽量走直线,避免嵌套分支,可以做这样的调整:
-
- 错误和异常处理
-
简单错误:仅出现一次,其他地方不需要捕获 可以使用erros.New来创建匿名变量来直接表示简单错误,比如
err := errors.New("emit macho dwarf: elf header corrupted")
。 也可以使用fmt.Errorf来格式化err := fmt.Errorf("user %q (id %d) not found", name, id)
-
wrap提供了一个error嵌套另一个error的能力,生成一个error的跟踪链,可以自己补充上下文,方便排查跟踪错误,在fmt.Errorf中使用
%w
关键字来将一个错误wrap至其错误链中。 -
错误判定 errors.As 判断一个错误是否是特定错误,可以使用
func Is(err, target error) bool
, 不同于==,它可以判定错误两行所有的错误是否包含特有错误而在错误链上获取某种错误,可以使用
func As(err error,target any) bool
,如果错误链上存在改种错误,则返回找到的第一个,否则返回false
-
- 控制流程
性能调优建议
- slice使用建议:
- 预分配内存: 尽可能在使用make初始化的时候提供容量信息。可以从c++的vector容器来理解这一点
- 另一个陷阱:大内存未释放
- 原切片较大,代码在原来切片的基础上新建小的切片(re-slice),由于原来的大切片在内存中有引用,所以它不能被回收释放,而导致占用较大的内存
- 可以使用
copy
函数代替re-slice (写一段测试代码)
- map的使用建议
- 预分配内存 :不断地向map 中添加元素的操作会触发map扩容,提前分配好空间可以减少内存拷贝和rehash的消耗
- 字符串的处理:使用strings.Builder
- 字符串的拼接:字符串在Go中是不可变类型,占用的内存大小固定,使用+每次都会重新分配内存,而strings.Builder底层是[]byte数组,存在内存扩容策略,不需要每次拼接都重新分配内存
- 使用空结构体节省内存空间
- 空结构体不占用任何内存空间,可作为各种场景下的占位符使用 使用栗子:用值为空结构体的map实现Set
使用pprof进行性能调优实战
我使用的是pprof自带的Web UI,以网页的形式呈现性能指标。可能需要先下载Graphviz组件。要处理的项目是"github.com/wolfogre/go-pprof-practice/" ,把它叫做炸弹代码吧,需要我们排查问题,优化代码。
运行项目后,浏览器输入:http://127.0.0.1:6060/debug/pprof/ 后进入如下页面
注意第一列是数量。 页面下方还有这些链接的具体解释:
- allocs 代表过去内存分配日的采样
- blocks 阻塞操作情况
- heap 堆上内存使用情况
- profile cpu占用资源
- mutex 锁竞争情况
- goroutine 当前所有goroutine的堆栈信息 由上图,我们可以从allocs,block,goroutine,mutex来排查问题。
终端运行go tool pprof -http=:8080 "http://127.0.0.1:6060/debug/pprof/heap"
点进source查看源代码分析,可以发现,*Mouse.steal()
这个函数每次向Buffer追加1M,直到达到1G
注释掉这个“炸弹”以后,我们可以发现alloc和heap都变成了5.
然后内存分配变得很正常! 恭喜成功拔出一枚炸弹!
可是,其实问题还没有被解决,这里展示的只是采样的时候被使用的内存(inuse),点击sample,选择alloc_space,就会发现还藏着一颗炸弹。
分析源码发现:
*Dog.run()
里面还有一个炸弹,每次申请16M的内存,申请了有没有使用,就被回收了,所以在inuse采样中看不见。这段程序已经累计申请了256M内存,啧啧啧。
注释掉这一段以后,再来看内存分配
flat 表示当前函数本身执行的消耗 cum 表示当前函数本身家伙是那个其调用函数的总消耗 因此, flat==cum,代表函数没有被其他函数调用 flat==0, 代表函数只有其他函数的调用
用同样的方法可以分析炸弹代码的其他问题,只要将go tool pprof -http=:8080 "http://127.0.0.1:6060/debug/pprof/xxxx
的xxx替换成要处理的内容,比如goroutine。
就先写到这儿,文章中的问题欢迎交流讨论:P