💌 这是我参与「第五届青训营」伴学笔记创作活动的第 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 的性能调优有哪些方法呢?
- 性能测试工具 benchmark 、test。
- slice 预分配内存,尽可能在使用 make() 初始化切片时提供容量信息,特别是在追加切片时。
- map 预分配内存。
- 使用 strings.Builder 进行字符串拼接:strings.Builder > bytes.Buffer > + 。
- 使用空结构体节省内存,不占内存空间。
- 使用 atomic 包保证多线程安全,通过硬件实现,效率比通过操作系统实现的锁高很多。
pprof(重点)
🍠 功能简介:pprof 是用于可视化和分析性能的工具,可以知道应用在什么地方耗费了多少 CPU、memory 等运行指标。
🧡 实战项目
💌 接下来我们使用 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/
🍠 使用 pprof 排查,输入如下命令进入交互式终端。
- 排查 CPU 占用过高
go tool pprof http://localhost:6060/debug/pprof/profile
top
flat:当前函数本身的执行耗时 (flat = 0 表示当前函数中只调用其它函数)
flat%:flat占CPU总时间的比例
sum%:上面每一行的flat%总和
cum:指当前函数本身加上其调用函数的总耗时 (flat = cum 表示当前函数没有调用其它函数)
cum%:cum占CPU总时间的比例
list Eat
可以排查出红框内一百亿次空循环占用了大量 CPU 时间,可以直接注释掉。
- 排查内存占用过高
go tool pprof http://localhost:6060/debug/pprof/heap
top
list Steal
可以排查出红框内有个循环一直向 m.buffer 追加 1Mb 的数组,直到总容量到达 1GB 为止,且一直不释放内存,占用大量内存空间,可以直接注释掉。
💌 剩下的问题按照文末的参考资料都能成功排查出来,这里就不多一一赘述~
- 排查频繁内存回收
- 排查协程泄露
- 排查锁的争用
- 排查阻塞操作
🧡 课后总结
💌 本节课主要讲了如何写出优雅的代码,以及如何调试代码中隐藏的问题,在之后的项目开发中需要特别关注,因为是团队协作,写出其他人也能看懂的代码尤为重要,好的开始是成功的一半!