本节课涉及到高质量编程中的以下几点:
- 什么是高质量编程?
- 高质量编程编码规范
- 性能优化建议
高质量编程
概念
编程的基本目标是完成对应的功能,但这远远称不上高质量编程。在编程时,不仅要达到对应的目标,还要达到可靠、简洁、清晰、低复杂性、可扩展、风格统一的目标。
Go语言开发者 Dave Cheney 关于高质量编程的观点:
- 简单性:要以简单清晰的逻辑编写代码,不理解的代码无法修复改进
- 可读性:代码是写给人看的,要编写可维护代码,首先要确保代码可读
- 生产力:团队整体工作效率十分重要
编码规范
像Google等很多大公司都推出了自己的编码规范。比较著名的如阿里巴巴的阿里巴巴Java编码规范和Google的Effective Go等等。在这里我们抽取一些公共的规范来介绍。
- 公共符号始终要注释
注释应该做的:
- 解释代码作用: 适合注释公共符号
- 解释代码如何做的: 适合注释实现过程
- 解释代码实现的原因: 适合解释代码的外部因素,提供额外上下文
- 解释代码什么情况下会出错: 适合解释代码的限制条件
重点:无论长度或复杂度,对外公开的任何符号都要进行注释。
代码是最好的注释,而注释的目的是提供代码之外的帮助信息。对于公共库函数,必须要做到事无巨细地介绍库中的每一处细节。所以对于程序中向外公开的任何符号都要进行注释。但是,对接口的实现函数不需要注释,因为在此处注释不能提供任何额外信息。
// 此函数根据输入的路径打开一个文件
// 如果成功找到并打开文件,将返回 File结构体和nil
// 如果在打开过程中发生错误,将返回nil和错误描述
// 具体错误描述请参考 error.go
func OpenFile(path string) (File, error)
func (f *File) Delete() error
- 代码格式
编写程序时要始终编写具有通用格式的代码,在这个方面,go语言为我们提供了 gofmt 工具,能够自动格式化 Go 代码为官方统一风格。常见的 IDE 都能很方便的完成 gofmt 工具的配置,例如 GoLand IDE 默认启用了在编辑结束后自动对 Go 文件格式化的功能。
Go 语言还为我们提供了 goimports 工具,作用在于自动增删依赖的包引用,将依赖包按字母序排序分类,并具有 gofmt 的全部功能。
- 命名规范
变量:
- 简洁胜于冗长
- 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
例如使用
ServeHTTP而不是ServeHttp,使用XMLHTTPRequest或者xmlHTTPRequest - 变量在定义时距离其被使用的地方越远,则需要携带越多的上下文信息 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义
例如,在常见的 for 循环中,上边的索引变量名 index 显然比下方的 i 冗长:
// Bad
for index := 0; index < len(s); index++ {
// Do something
}
// Good
for index := 0; index < len(s); index++ {
// Do something
}
在上面的代码中,index 这一变量名没有增加对程序的理解,反而导致程序变得冗长了。
此外,在对外提供的函数的参数或全局变量中,最好令变量名带有更多的信息:
// Good
func (c *Client) send(req *Request, deadline time.Time)
// Bad
func (c *Client) send(req *Request, t time.Time)
t 这个名字可以指任意时间,不如 deadline 更加精确和有意义。
函数:
- 函数名不携带包名的信息,因为包名和函数名总是成对出现的
- 函数名应当尽量简短
- 当名为
foo的包的某个函数返回类型和包名无关系时,可以在函数名中加入返回类型信息
例如,在下面的两个函数中,明显第一个函数更好。
package http
// Good
func Serve(l net.Listener, handler Handler) error
// Bad
func ServeHTTP(l net.Listener, handler Handler) error
包名:
- 只由小写字母组成,不包含大写字母和下划线等
- 简短并包含一定的上下文信息
- 不要与已有的库(例如标准库)同名
- 不使用常用变量名,包名要有特点
- 使用单数
- 谨慎使用缩写
总体来说,命名规范的核心是降低阅读理解代码的成本,重点考虑上下文信息,设计简洁清晰的名称。
- 控制流程
避免嵌套,保持正常流程清晰
在下面的示例中,如果两个分支中都包含 return 语句,则可以去除冗余的 else:
// Bad
if foo {
return x
} else {
return nil
}
// Good
if foo {
return x
}
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 {
err := doSomething()
if err != nil {
return err
}
err = doAnotherThing()
if 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 实际上提供了一个错误嵌套另一个错误的能力,从而生成了一个错误的跟踪链
- 在
fmt.Errorf中使用%w关键字来将一个错误关联到错误链中
result, err := doSomething()
if err != nil {
return fmt.Errorf("error when do something: %w", err)
}
错误判定
- 判定一个错误是否为特定错误,使用
errors.Is - 不同于使用
==, 使用该方法可以判断错误链上的所有错误中是否含有特定错误
data, err := loadDataFromFile("PATH")
if errors.Is(err, fs.ErrNotExist) {
// File not found at filesystem
data.NewFile()
}
- 在错误链上获取特定种类的错误,使用
errors.As
data, err := loadDataFromFile("PATH")
if err != nil {
var pathError *fs.PathError
if errors.As(err, &pathError) {
fmt.Println("Failed to load file at path:", pathError.Path)
} else {
return nil
}
}
panic 的使用
- 不建议在业务代码中使用 panic
- 调用函数若不包含 recover 会造成程序崩溃
- 若问题可以被屏蔽或解决,建议使用 error
- 当程序在启动节点发生不可逆转的错误时,可以在 init 或 main 函数中使用 panic
recover 的使用
- recover 只能在被 defer 的函数中使用
- 嵌套无法生效
- 只在当前 goroutine 生效
- defer 的语句是先进后出
recover 的一个重要作用是在 panic 后记录重要信息