编写高质量的 Go 代码~
前言:
本次课程简要介绍了高质量编程的定义和原则,分享了代码格式、注释、命名规范、控制流程、错误和异常处理五方面的常见编码规范,帮助我们在今后的开发过程中写出更加优秀的代码 …
什么是高质量编程?
编写的代码能够达到正确可靠、简洁清晰的目标可称之为高质量代码,一份高质量的代码应该具备以下特点:
- 各种边界条件考虑完备。
- 异常情况处理,稳定性保证。
- 易读易维护。
编程原则:
Go 语言开发者 Dave Cheney 给出了三条编程原则,在编程中我们应该尽可能遵循这些原则。
- 简单性:消除“多余的复杂性”,以简单清晰的逻辑写代码。
- 可读性:编写可维护代码的第一步是确保代码可读。
- 生产力:团队整体工作效率非常重要。
编码规范
如何编写高质量的 Go 代码?
注释:
包中声明的每个公共的符号(变量、常量、函数…)都要添加注释;任何既不明显也不简短的公共功能必须予以注释;无论长度或复杂度如何,对库中的任何函数都必须进行注释。
注释应该解释代码的作用、代码是如何做的以及代码的实现原因,还应该解释代码什么情况会出错。
🎈比如 Go 的标准库中对于函数也有注释来说明功能:
// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call returns err == nil, not err == EOF. Because ReadAll 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) {
b := make([]byte, 0, 512)
for {
if len(b) == cap(b) {
// Add more capacity (let append pick how much).
b = append(b, 0)[:len(b)]
}
n, err := r.Read(b[len(b):cap(b)])
b = b[:len(b)+n]
if err != nil {
if err == EOF {
err = nil
}
return b, err
}
}
}
(PS:有一个例外,不需要注释实现接口的方法。)
代码格式:
推荐使用 gofmt 自动格式化代码。
gofmt 是 Go 语言官方提供的工具,能自动格式化 Go 语言代码为官方统一风格,常见 IDE 都支持方便的配置。此外 goimports 也是 Go 语言官方提供的工具,可以实现自动增删依赖的包引用、将依赖包按字母序排序并分类。
🎈在 GoLand 中开启 gofmt 支持:
GoLand 提供了 File Watchers 功能,将 go fmt 添加进去,修改触发的条件即可。
配置完成后每次保存代码时 go fmt 就会自动格式化代码。
命名规范
1. 变量:
- 简洁胜于冗长。
- 缩略词全大写(比如
ServeHTTP
),但当其位于变量开头且不需要导出时,使用全小写(比如xmlHTTPRequest
)。 - 变量距离其被使用的地方越远,则需要携带越多的上下文信息。
2. 函数:
- 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的。
- 函数名尽量简短。
- 当名为
foo
的包某个函数返回类型Foo
时,可以省略类型信息而不导致歧义。 - 当名为
foo
的包某个函数返回类型T
时(T 并不是 Foo),可以在函数名中加入类型信息。
3. package:
- 只由小写字母组成,不包含大写字母和下划线等字符。
- 简短并包含一定的上下文信息,例如 schema、task 等。
- 不要与标准库同名。
流程控制
流程控制语句应该优先处理错误情况、特殊情况,尽早返回或继续循环来减少嵌套。
✔原则:尽量保持正常代码路径为最小缩进。
比如下面的代码就是一个错误的示范:
// Bad
func OneFunc() error {
err := doSomething()
if err == nil {
err := doAnotherThing()
if err == nil {
return nil // normal case
}
return err
}
return err
}
- 这段代码正常的流程路径被嵌套在两个
if
条件内,成功退出的条件是return nil
,必须仔细匹配大括号才能发现; - 函数最后一行返回一个错误,需要追溯到匹配的左括号,才能了解何时会触发错误。
- 并且如果后续正常流程需要增加一步操作,调用新的函数,则又要增加一层嵌套。
调整后的代码如下:
// Good
func OneFunc() error {
if err := doSomething(); err != nil {
return err
}
if err := doAnotherThing(); err != nil {
return err
}
return nil // normal case
}
编写流程控制代码时要尽可能遵循线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支。
错误和异常处理
1. 简单错误:
- 简单的错误指仅出现一次的错误,且在其他地方不需要捕获该错误。
- 优先使用
errors.New()
来创建匿名变量来直接表示简单错误。 - 如果有格式化的需求,使用
fmt.Errorf()
。
💻 Github 仓库中的示例代码:
func defaultCheckRedirect(req *Request, via []*Request) error {
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
return nil
}
2. 错误的 Wrap 和 Unwrap:
- 错误的 Wrap 实际上是提供了一个 error 嵌套另一个 error 的能力,从而生成一个 error 的跟踪链。
- 在
fmt.Errorf()
中使用%w
关键字来将一个错误关联至错误链中。
💻 Github 仓库中的示例代码:
list, _, err := c.GetBytes(cache.Subkey(a.actionID, "srcfiles"))
if err != nil {
return fmt.Errorf("reading srcfiles list: %w", err)
}
3. 错误判定:
- 在错误链上获取特定种类的错误,使用
errors.AS()
。
💻 Github 仓库中的示例代码:
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)
}
}
4. panic & recover:
Go语言不支持传统的 try…catch…finally
这种异常,但是 Go 中可以抛出一个 panic 的异常,然后在 defer 中通过 recover 捕获这个异常,然后正常处理。
✔ panic 的注意事项:
- 当程序启动阶段发生不可逆转的错误时,可以在 init 或 main 函数中使用
painc()
。 - 不建议在业务代码中使用
panic()
,若问题可以被解决或屏蔽,建议使用error
替代。
✔ recover 的注意事项:
recover()
只能在被 defer 的函数中使用。- 嵌套无法生效。
- 只在当前 goroutine 生效。
🎈补充 - Go 中 defer 的概念:
Go 语言的 defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行。