这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天
本节主要简要介绍了高质量编程的定义和原则,分享了代码格式、注释、命名规范、控制流程、错误和异常处理五方面的常见编码规范。目标主要达成以下四点:如何编写更简洁清晰的代码;常用Go语言程序优化手段;熟悉Go程序性能分析工具;了解工程中性能优化的原则和流程。
高质量编程
什么是高质量——编写的代码能够达到正确可靠、简洁清晰的目标可称之为高质量代码
- 各种边界条件是否考虑完备
- 异常情况处理,稳定性保证
- 易读易维护
编程原则
实际应用场景千变万化,各种语言的特性和语法各不相同,但是高质量编程遵循的原则是相通的
简单性
- 消除“多余的复杂性”,以简单清晰的逻辑编写代码
- 不理解的代码无法修复改进
可读性
- 代码是写给人看的,而不是机器
- 编写可维护代码的第一步是确保代码可读
生产力
- 团队整体工作效率非常重要
编码规范
如何编写高质量的Go代码
代码格式
推荐使用gofmt自动格式化代码
注释
-
注释应该做的
- 注释应该解释代码作用
- 注释应该解释代码如何做的
- 注释应该解释代码实现的原因
- 注释应该解释代码什么情况会出错
公共符号始终要注释
- 包中声明的每个公共的符号:变量、常量、函数以及结构都需要添加注释
- 任何既不明显也不简短的公共功能必须予以注释
- 无论长度或复杂程度如何,对库中的任何函数都必须进行注释
// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call return err == nil, not err == EOF. Because ReadAll is
// is defined to read from src until EOF, it does not treat an EOF from Read
// as an error to be reported.
func ReadAll(r Reader) ([]byte, error)
- 有一个例外,不需要注释实现接口的方法。具体不要像下面这样做
// Read implements the io.Reader interface
func (r *FileReader) Read(buf []byte) (int, error)
命名规范
变量命名
-
简洁胜于冗长
-
缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
- 例如使用ServeHTTP而不是ServeHttp
- 使用XMLHTTPRequest或者xmlHTTPRequest
-
变量距离其被使用的地方越远,则需要携带越多的上下文信息
- 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义
函数命名
- 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的
- 函数名尽量简短
- 当名为foo的包某个函数返回类型Foo时,可以省略类型信息而不导致歧义
- 当名为foo的包某个函数返回类型T时(T并不是Foo),可以在函数名中加入类型信息
包命名
- 只用小写字母组成。不包含大写字母或下划线等字符
- 简短并包含一定的上下文信息。例如schema、task等
- 不要与标准库同名。例如不要使用sync或者strings
以下规则尽量满足,以标准库包名为例
- 不使用常用变量名作为包名。例如使用bufio而不是buf
- 使用单数而不是复数。例如使用encoding而不是encodings
- 谨慎地使用缩写。例如使用fmt在不破坏上下文的情况下比format更加简短
流程控制
避免嵌套,保持正常的流程清晰
如果两个分支都包含return语句,则可以去掉冗余的else
// Bad
if foo {
return x
} else {
return nil
}
// Good
if foo {
return x
}
return nil
尽量保持正常代码路径为最小缩进,优先处理错误情况/特殊情况,并尽早返回或继续循环来减少嵌套,增加可读性
- 最常见的正常流程的路径被嵌套在两个if条件内
- 成功的退出条件是return nil,必须仔细匹配大括号来发现
- 函数最后一行返回一个错误,需要追溯到匹配的左括号,才能了解何时会触发错误
- 如果后续正常流程需要进一步添加一步操作,调用新的函数,则又会增加一层嵌套
// Bad
func OneFunc() error {
err := doSomething()
if err == nil {
err := doAnotherThing()
if err == nil {
return nil // normal case
}
return err
}
return err
}
// Good
func OneFunc() error {
if err := doSomething(); err != nil {
return err
}
if err := doAnotherThing(); err != nil {
return err
}
return nil // normal case
}
错误和异常处理
简单错误处理 —— 仅出现一次的错误,且在其他地方不需要捕获该错误
- 优先使用 errors.New 来创建匿名变量来直接表示简单错误
- 如果有格式化的需求,使用 fmt.Errorf
func defaultCheckRedirect(req *Request, via []*Request) error {
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
return nil
}
错误的Wrap和Unwrap
- 错误的Wrap实际上是提供了一个error嵌套另一个error的能力,从而生成一个error的跟踪链
- 在 fmt.Errorf 中使用: %w 关键字来将一个错误关联至错误链中
-
Go1.13 在 errors 中新增了三个新 API 和一个新的 format 关键字,分别是 errors.Is、errors.As 、errors.Unwrap 以及 fmt.Errorf 的 %w。如果项目运行在小于 Go1.13 的版本中,导入 golang.org/x/xerrors 来使用。以下语法均已 Go1.13 作为标准。
list, _, err := c.GetBytes(cache.Subkey(a.actionID, "srcfiles"))
if err != nil {
return fmt.Errorf("reading srcfiles list: %w", err)
}
错误判定
- 判断一个错误是否为特定错误,使用 errors.Is
- 不同于使用==,使用该方法可以判断错误链上的所有错误是否含有特定的错误
data, err = lockedfile.Read(targ)
if errors.Is(err, fs.ErrNotExist) {
// Treat non-existent as empty, to bootstrap the "latest" file
// the first time we connect to a given database.
return []byte{}, nil
}
return data, err
- 在错误链上获取特定种类的错误,使用 errors.As
if _, err := os.Open("non-existing"); err != nil {
var pathError *fs.PathError
if errors.As(err, &pathError) {
fmt.Println("Failed at path:", pathError.Path)
} else {
fmt.Println(err)
}
}
panic
- 不建议在业务代码中使用panic
- 调用函数不包含recover会造成程序崩溃
- 若问题可以被屏蔽或解决,建议使用error代替panic
- 当程序启动阶段发生不可逆转的错误时,可以在init或main函数中使用panic
func main() {
// ...
ctx, cancel := context.WithCancel(context.Background())
client, err := sarama.NewConsumerGroup(strings.Split(brokers, ","), group, config)
if err != nil {
log.Panicf("Error creating consumer group client: %v", err)
}
// ...
}
recover
- recover 只能在被defer的函数中使用
- 嵌套无法生效
- 只在当前goroutine生效
- defer的语句是后进先出
func (s *ss) Token(skipSpace bool, f func(rune) bool) (tok []byte, err error) {
defer func() {
if e := recover(); e != nil {
if se, ok := e.(scanError); ok {
err = se.err
} else {
panic(e)
}
}
}()
// ...
}
- 如果需要更多的上下文信息,可以recover后在log中记录当前的调用栈
func (t *treeFS) Open(name string) (f fs.File, err error) {
defer func() {
if e := recover(); e != nil {
f = nil
err = fmt.Errorf("gitfs panic : %v\n%s", e, debug.Stack())
}
}()
// ...
}
总结
初次之外,还可以了解一些Standard Go Prject Layout(github.com/golang-stan…
This is a basic layout for Go application projects. It's not an official standard defined by the core Go dev team; however, it is a set of common historical and emerging project layout patterns in the Go ecosystem. Some of these patterns are more popular than others. It also has a number of small enhancements along with several supporting directories common to any large enough real world application.
一个中文版本 Go 面向包的设计和架构分层(github.com/danceyoung/…
性能调优工具
性能调优原则
- 要依靠数据而不是猜测
- 要定位最大瓶颈而不是细枝末节
- 不要过早优化
- 不要过度优化
性能分析工具 pprof
- 希望知道应用在什么地方耗费了多少CPU、Memory
- pprof是用于可视化和分析性能分析数据的工具
http://localhost:6060/debug/pprof/
go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
- flat:当前函数本身的执行耗时
- flat%:flat占CPU总时间的比例
- sum%:上面每一行的flat%总和
- cum:指当前函数本身加上其调用函数的总耗时
- cum%:cum占CPU总时间的比例
(pprof) top
Showing nodes accounting for 3320ms, 100% of 3320ms total
flat flat% sum% cum cum%
3190ms 96.08% 96.08% 3320ms 100% github.com/wolfogre/go-pprof-practice/animal/felidae/tiger.(*Tiger).Eat
130ms 3.92% 100% 130ms 3.92% runtime.asyncPreempt
0 0% 100% 3320ms 100% github.com/wolfogre/go-pprof-practice/animal/felidae/tiger.(*Tiger).Live
0 0% 100% 3320ms 100% main.main
0 0% 100% 3320ms 100% runtime.main
排查 CPU 问题
-
命令行分析
-
go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"
-
-
top 命令
-
list 命令
-
熟悉 web 页面分析
-
调用关系图,火焰图
-
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/cpu"
排查堆内存问题
-
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap]"
排查协程问题
-
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"
排查锁问题
-
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"
排查阻塞问题
-
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"
性能优化案例
业务服务优化
- 服务:能单独部署,承载一定功能的程序
- 依赖:Service A的功能实现依赖Service B的响应结果,称为Service A依赖Service B
- 调用链路:能支持一个接口请求的相关服务集合及其相互之间的依赖关系
- 基础库:公共的工具包、中间件
流程
- 建立服务性能评估手段
- 分析性能数据,定位性能瓶颈
- 重点优化项改造
- 优化效果验证
建立服务性能评估手段
-
服务性能评估手段
- 单独 benchmark 无法满足复杂逻辑分析
- 不同负载情况下性能表现差异
-
请求流量构造
- 不同请求参数覆盖逻辑不同
- 线上真实流量情况
-
压测范围
- 单机器压测
- 集群压测
-
性能数据采集
- 单机性能数据
- 集群性能数据
分析性能数据,定位性能瓶颈
- pprof火焰图
重点优化项分析
进一步优化,服务整体链路分析
- 规范上游服务调用接口,明确场景需求
- 分析链路,通过业务流程优化提升服务性能
基础库优化
AB实验SDK的优化
-
分析基础库核心逻辑和性能瓶颈
- 设计完善改造方案
- 数据按需获取
- 数据序列化协议优化
-
内部压测验证
-
推广业务服务落地验证
Go语言优化
编译器&运行时优化
- 优化内存分配策略
- 优化代码编译流程,生成更高效的程序
- 内部压测验证
- 推广业务服务落地验证