简介
高质量代码指的是正确可靠、简洁清晰的代码,对各种异常情况、边界条件都尽量考虑完备,具有较好的可读性和维护性,同时具有简单性和生产力。简单性指的是简单清晰的逻辑,可读性是非常重要的,而生产力在团队整体工作效率中非常重要。而如何编写高质量代码,可以从代码规范做起。
1、高质量编程与代码规范
(1) 代码格式
Go语言官方提供了工具——gofmt,其能自动格式化Go语言代码为官方统一风格。
使用方式为:
go fmt <文件名>.go。
而在Goland里,可以这样使用:
goimports也是Go语言官方提供的工具,等于gofmt加上依赖包管理,自动增删依赖的包引用,将依赖包按字母排序并分类。
(2) 注释
很多时候我们在不该写注释的地方写了注释,又在该写注释的地方没写,那么,注释应该在什么时候写呢?
-
注释应该解释代码作用(适合注释公共符号)
-
注释应该解释代码如何做(适合注释实现过程)
但是不需要在一些特别简单的地方(即无法提供代码未提供出的上下文信息)写注释,反例如下:
- 注释应该解释代码实现的原因(适合解释外部因素)
具体来说,即"why",适合提供额外的上下文信息,帮助读者读取从代码中看不出的上下文环境,从而更加容易理解。
- 注释应该解释代码什么情况下会出错(适合解释限制条件)
- 公共符号始终要添加注释
公共符号即包中的每个公共符号(变量常量函数结构等),不明显的公共功能也需要注释。除此以外,库中的任何函数都需要注释。
(3) 命名规范
变量
- 简洁优于冗长
//不好.因为作用域限制在for循环内,将i冗长为index对理解代码没有任何帮助
for index:=1;index<=n;index++ {
}
//ok
for i:=1;i<=n;i++ {
}
- 缩略词全大写
举例来说,使用ServeHTTP而不是ServeHttp,有一个特例是当其位于开头且不需要导出时(导出需要首字母大写),使用全小写,如xmlHTTPRequest。
- 变量离其被使用的地方越远,则需要携带越多的上下文信息。
//ok.deadline具有特定的含义
func send(deadline time.Time) {
}
//not ok.t一般指代粗略的时间,在需要使用的时候丢失了上下文信息.
func send(t time.Time) {
}
函数
- 函数名不携带包名的上下文信息
package foo
import (
"fmt"
)
//ok.
func add(a,b int) int {
return a+b
}
//不好,已经是foo包里的函数了.
func fooadd(a,b int) int {
return a+b
}
- 函数名尽量简短
func getUser() string {
}
func getUserinfo() string {
}
-
当函数返回值为包名相关类型时可省略
-
包内某函数返回类型为
T时可以在函数名中加入类型信息。
包
- 只有小写字母组成。不包含大写字母和下划线等字符。
- 简短并包含一定的上下文信息,如
schema、task等。 - 不要与标准库同名,例如不要使用
sync。
除此以外,尽量遵循以下规则:
- 不要用常用变量名作包名,如使用
bufio而非buf。 - 使用单数而非复数,如
encoding。 - 慎用缩写。
fmt包就在不破坏上下文的基础上更为简洁。
(4) 控制流程
- 避免无意义的嵌套
一个很简单的例子:若两个分支都会return某些值,那么可以去除其中一个else。
func process(ok bool) string {
if ok {
return "ok"
} else {
return "not ok"
}
}
//应改为如下代码:
func process(ok bool) string {
if ok {
return "ok"
}
return "not ok"
}
- 尽量保持代码流程为最小缩进,避免嵌套
func process() error {
err:=do1()
if err==nil {
err = do2()
if err==nil {
return nil
}
return err
}
return err
}
显然上面的代码把简单的逻辑嵌套得太多了,我们完全可以边判断边返回,不需要嵌套在一起。
func process() error {
if err:=do1();err!=nil {
return err
}
if err:=do2();err!=nil {
return err
}
return nil
}
(5) 错误和异常处理
简单错误
对于简单错误,即仅出现一次,且不需要在其他地方被捕获,那么我们可以直接用匿名变量:
当我们有格式化需求时,也可以使用fmt.errorf()来创建实现 error 接口的错误对象。
func process(a []int) error {
n := len(a)
for i := 0; ; i++ {
if i >= n {
return fmt.Errorf("Index %d is out of bound!", i)
}
}
return nil
}
错误链
考虑这样一个场景,我们在错误的传递中需要给错误更多的上下文信息,即在某类错误中具体又属于哪一类错误呢?即错误的嵌套。因此我们引入了错误链。
在Errorf()里使用%w关键字,可以将一个错误关联到已有的错误链中。
func process(file string) error {
data, err := os.ReadFile(file)
if err != nil {
return fmt.Errorf("failed to read file: %w", err)
}
fmt.Println(data)
return nil
}
除此之外,Go语言还提供了Unwrap返回错误链的下一个错误,errors.Is()函数判断错误链上是否包含特定的错误。
func process(filename string) error {
_, err := os.ReadFile(filename)
if err != nil {
return fmt.Errorf("failed to read file: %w", err)
}
return nil
}
func main() {
err := process("hello.txt")
if err != nil {
fmt.Println(err)
fmt.Println(errors.Is(err, os.ErrNotExist)) //文件不存在的错误.错误链中存在.为True.
//依次解包错误.
err = errors.Unwrap(err)
fmt.Println(err)
err = errors.Unwrap(err)
fmt.Println(err)
return
}
}
panic
不建议在业务代码中使用panic。在有panic而不含recover时会使得程序宕机崩溃。因此,若问题可被屏蔽或解决,建议使用error代替。
recover
recover只能在defer的函数中使用,无法嵌套,且只在当前goroutine生效。
如果当前goroutine进入panic时,recover函数的返回值就是panic的输入值,并使得程序停止向上传播panic,不会造成程序崩溃。
func RecoverRun(f func()) {
defer func() {
err := recover()
fmt.Println("Error", err)
}()
f()
}
func main() {
RecoverRun(func() {
fmt.Println("宕机前")
panic("这是一个错误!")
fmt.Println("宕机后") //recover后不会执行当前goroutine剩下的代码.
})
fmt.Println("Hello")
}
如上代码所示,panic并没有使整个程序崩溃,只是退出了发生panic的goroutine而已,剩下的部分仍然执行完成了。