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

97 阅读6分钟

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

🧡 本堂课重点内容

  • 如何编写更简洁清晰的代码
  • 常用Go语言程序优化手段
  • 熟悉Go程序性能分析工具
  • 了解工程中性能优化的原则和流程

🧡 知识点介绍

高质量编程

💌 编写的代码能够达到正确可靠、简洁清晰的目标可称之为高质量代码。高质量编程遵循的原则是相通的:

  • 简单性
    消除“多余的复杂性”,以简单清晰的逻辑编写代码
    不理解的代码无法修复改进
  • 可读性
    代码是写给人看的,而不是机器
    编写可维护代码的第一步是确保代码可读生产力
  • 生产力
    团队整体工作效率非常重要

💌 如何编写高质量的 Go 代码呢?要符合如下编码规范:

  • 代码格式
    1、gofmt:推荐使用,Go语言官方提供的工具,能自动格式化 Go 语言代码为官方统一风格,常见 IDE 都支持。
    2、goimports:Go语言官方提供的工具,相当于 gofmt + 依赖包管理,自动增删依赖包引用,将依赖包按字母顺序排序并分类。
  • 注释
    1、包中声明的每个公共符号:变量、常量、函数以及结构都需要添加注释。
    2、任何既不明显也不简短的公共功能必须予以注释。
    3、无论长度或复杂程度如何,对库中的任何函数都必须进行注释。
    4、注释应该解释代码作用、实现过程、实现原因、限制条件
  • 命名规范
    变量名
    1、简洁胜于冗长。
    2、缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写:如 ServeHTTP、xmlHTTPRequest。
    3、变量距离其被使用的地方越远(如全局变量),则需要携带越多的上下文信息。
    函数名
    1、函数名尽量简短。
    2、函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的。
    3、当名为 foo 的包某个函数返回类型 Foo 时,可以省略类型信息而不导致歧义;返回类型 T 时(T 并不是 Foo),可以在函数名中加入类型信息。
    包名
    1、只由小写字母组成,不包含大写字母和下划线等字符。
    2、简短并包含一定的上下文信息,例如 schema、task 等。
    3、不要与标准库同名,例如不要使用 sync 或者 strings。
    4、不使用常用变量名作为包名。
    5、使用单数而不是复数。
    6、谨慎地使用缩写。
  • 控制流程
    1、避免嵌套,保证正常流程清晰。
    2、尽量保持正常代码路径为最小缩进,优先处理错误情况/特殊情况,尽早返回或继续循环来减少嵌套。
  • 错误和异常处理
    简单错误
    1、简单错误指仅出现一次的错误,且在其他地方不需要捕获该错误。
    2、优先使用 errors.New 来创建匿名变量直接表示简单错误。
    3、如果有格式化的需求,使用fmt.Errorf。
    错误的 Wrap 和 Unwrap
    1、错误的 Wrap 实际上是提供了一个 error 嵌套另一个 error 的能力,从而生成一个 error 跟踪链。
    2、在 fmt.Errorf 中使用 %w 关键字来将一个错误关联至错误链中。
    错误判定
    1、判定一个错误是否为特定错误,使用 errors.ls,不同于使用 == ,使用该方法可以判定错误链上的所有错误是否含有特定的错误。
    2、在错误链上获取特定种类的错误,使用 errors.As
    3、当程序启动阶段发生不可逆转的错误时,可以在 init 或 main 函数中使用 panic,不建议在业务代码中使用。
    4、引用其他的库抛出 panic 时,用 recover 进行处理,只能在当前 goroutine 被 defer 的函数中使用。如果需要更多的上下文信息,可以 recover 后在 log 中记录当前的调用栈。

性能调优

💌 在编写完正确可靠、简洁清晰的高质量代码之后,考虑时间效率和空间效率需要对代码进行性能优化,Go 的性能调优有哪些方法呢?

  • 性能测试工具 benchmarktest
  • slice 预分配内存,尽可能在使用 make() 初始化切片时提供容量信息,特别是在追加切片时。
  • map 预分配内存。
  • 使用 strings.Builder 进行字符串拼接:strings.Builder > bytes.Buffer > + 。
  • 使用空结构体节省内存,不占内存空间。
  • 使用 atomic 包保证多线程安全,通过硬件实现,效率比通过操作系统实现的锁高很多。

pprof(重点)

🍠 功能简介:pprof 是用于可视化和分析性能的工具,可以知道应用在什么地方耗费了多少 CPU、memory 等运行指标。

image.png

🧡 实战项目

💌 接下来我们使用 pprof 工具定位一个炸弹程序中的问题。

🍠 下载程序到本地 wolfogre/go-pprof-practice: go pprof practice. (github.com)

main 函数如下:

package main

import (
   "log"
   "net/http"
   _ "net/http/pprof" // 会自动注册 handler 到 http server,方便通过 http 接口获取程序运行采样报告
   "os"
   "runtime"
   "time"

   "github.com/wolfogre/go-pprof-practice/animal"
)

func main() {
   log.SetFlags(log.Lshortfile | log.LstdFlags)
   log.SetOutput(os.Stdout)

   runtime.GOMAXPROCS(1)              // 限制 CPU 使用数,避免过载
   runtime.SetMutexProfileFraction(1) // 开启对锁调用的跟踪
   runtime.SetBlockProfileRate(1)     // 开启对阻塞操作的跟踪

   go func() {
      // 启动一个 http server,注意 pprof 相关的 handler 已经自动注册过了
      if err := http.ListenAndServe(":6060", nil); err != nil {
         log.Fatal(err)
      }
      os.Exit(0)
   }()

   for {
      for _, v := range animal.AllAnimals {
         v.Live()
      }
      time.Sleep(time.Second)
   }
}

🍠 编译并运行程序,一分钟后电脑还扛得住,保持程序运行,打开浏览器访问:http://localhost:6060/debug/pprof/ image.png

🍠 使用 pprof 排查,输入如下命令进入交互式终端。

  • 排查 CPU 占用过高
go tool pprof http://localhost:6060/debug/pprof/profile

image.png top image.png

flat:当前函数本身的执行耗时 (flat = 0 表示当前函数中只调用其它函数)
flat%:flat占CPU总时间的比例
sum%:上面每一行的flat%总和
cum:指当前函数本身加上其调用函数的总耗时 (flat = cum 表示当前函数没有调用其它函数)
cum%:cum占CPU总时间的比例

list Eat image.png 可以排查出红框内一百亿次空循环占用了大量 CPU 时间,可以直接注释掉。

  • 排查内存占用过高
go tool pprof http://localhost:6060/debug/pprof/heap

top image.png list Steal image.png 可以排查出红框内有个循环一直向 m.buffer 追加 1Mb 的数组,直到总容量到达 1GB 为止,且一直不释放内存,占用大量内存空间,可以直接注释掉。

💌 剩下的问题按照文末的参考资料都能成功排查出来,这里就不多一一赘述~

  • 排查频繁内存回收
  • 排查协程泄露
  • 排查锁的争用
  • 排查阻塞操作

🧡 课后总结

💌 本节课主要讲了如何写出优雅的代码,以及如何调试代码中隐藏的问题,在之后的项目开发中需要特别关注,因为是团队协作,写出其他人也能看懂的代码尤为重要,好的开始是成功的一半!

🧡 参考资料