Go 语言高质量编程与性能调优 | 青训营笔记

30 阅读6分钟

高质量编程以及性能优化入门

这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天。收获颇丰嘿嘿!

高质量编程

如何编写高质量的Go代码

  • 注释:
    • 合理的注释公共符号(比如对外提供的函数,利用注释描述他的功能)
    • 对代码中复杂的,并不明显的逻辑进行说明
    • 解释代码的外部因素,这些因素脱离上下文以后很难理解
    • 提醒使用者一些潜在的限制条件或者会无法处理的情况:比如是否有安全隐患,输入限制
    • 总结:代码是最好的注释,注释应该提示出代码没有表达出的上下文信息
  • 命名规范
    • 函数:
      • 函数名不要携带包名的上下文信息,因为包名和函数名总是成对出现的

        举个栗子:http包中创建服务的函数命名成 Serve比ServeHTTp好,因为在实际在调用http包Serve方法的时候,写的代码http.Serve,会携带有http包名,所以函数名字就不需要再包含http了。

      • 当名为foo包的某个函数返回Foo时,可以省略类型信息而不导致歧义

      • 当名为foo包的某个函数返回其他类型(不是Foo)的时候可以在函数命名中加入类型信息

        (后面两条有待理解 T.T)

    • 包:
      1. 由小写字母组成,不包含大写字母和下划线
      2. 不要与标准库同名
  • 编码规范
    • 控制流程
      • 避免嵌套,保持正常流程清晰。比如if语句里两个都包含return,可以除去冗余的else

      • 优先处理错误情况,尽早返回或继续循环来减少嵌套 (代码解释见下)

        image.png

        正常流程的代码被嵌在两个if里面,这样有很多弊端:1.很难发现成功退出的条件 2.函数如果返回一个错误,要追溯到匹配的左括号,才能了解什么时候触发的错误 3. 正常流程还要增加一步,调用新的函数,那就又要新增一层嵌套

      • 所以我们写的时候要遵循线性原理,逻辑尽量走直线,避免嵌套分支,可以做这样的调整:

        image.png

    • 错误和异常处理
      • 简单错误:仅出现一次,其他地方不需要捕获 可以使用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/ 后进入如下页面 image.png

注意第一列是数量。 页面下方还有这些链接的具体解释:

  • 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"

image.png

点进source查看源代码分析,可以发现,*Mouse.steal()这个函数每次向Buffer追加1M,直到达到1G

image.png

注释掉这个“炸弹”以后,我们可以发现alloc和heap都变成了5.

然后内存分配变得很正常! 恭喜成功拔出一枚炸弹! image.png

可是,其实问题还没有被解决,这里展示的只是采样的时候被使用的内存(inuse),点击sample,选择alloc_space,就会发现还藏着一颗炸弹。 image.png

分析源码发现: *Dog.run()里面还有一个炸弹,每次申请16M的内存,申请了有没有使用,就被回收了,所以在inuse采样中看不见。这段程序已经累计申请了256M内存,啧啧啧。 image.png

注释掉这一段以后,再来看内存分配

image.png

flat 表示当前函数本身执行的消耗 cum 表示当前函数本身家伙是那个其调用函数的总消耗 因此, flat==cum,代表函数没有被其他函数调用 flat==0, 代表函数只有其他函数的调用

用同样的方法可以分析炸弹代码的其他问题,只要将go tool pprof -http=:8080 "http://127.0.0.1:6060/debug/pprof/xxxx的xxx替换成要处理的内容,比如goroutine。

就先写到这儿,文章中的问题欢迎交流讨论:P