这是我参与「第三届青训营 -后端场」笔记创作活动的第3篇笔记。
Go 编码规范
编写的代码能够达到正确可靠、简洁清晰的目标可称之为高质量代码。
- 各种边界条件是否考虑完备
- 异常情况处理,稳定性保证
- 易读易维护
原则
实际应用场景千变万化,各种语言的特性和语法各不相同,但高质量编程遵循的原则是相通的。
- 简单性
- 消除“多余的复杂性”,以简单清晰的逻辑编写代码
- 不理解的代码无法修复改进
- 可读性
- 代码是写给人看的,而不是机器
- 编写可维护代码的第一步是确保代码可读
- 生产力
- 团队整体工作效率非常重要
编码规范
代码格式
使用 gofmt 自动格式化代码,使用 goimports 自动增删依赖的包引用以及将依赖包按字母序排序并分类。
- gofmt 是 Go 官方提供的工具,能自动格式化 Go 代码为官方统一风格
- goimports 也是 Go 官方提供的工具,goimports = gofmt + 依赖包管理
注释
Good code has lots of comments, bad code requires lots of comments.
——Dave Thomas and Andrew Hunt
注释应该做的事情:
- 解释代码作用
- 解释代码如何做的
- 解释代码实现的原因
- 解释代码什么情况会出问题
解释代码作用
-
适合注释公共符号
公共符号只是一些常量,变量,对外提供的一些函数等。
// Open opens the named file for reading. If successful, methods on // the returned file can be used for reading; the associated file // descriptor has mode O_RDONLY. // If there is an error, it will be of type *PathError. func Open(name string) (*File, error) { return OpenFile(name, O_RDONLY, 0) }如果函数比较简单或函数名已经解释了函数的作用,则不需要额外添加注释,如:
// Returns true if the table cannot hold any more entries func IsTableFull() bool
解释代码如何做的
-
适合注释实现过程
这一段代码的作用是给 URL 加上 Referer 信息,但直接看代码对功能的描述可能不太明显,所以加上注释解释实现的功能
// Add the Referer header from the most recent // request URL to the new one, if it's not https->http: if ref := refererForURL(reqs[len(reqs)-1].URL, req.URL); ref != "" { req.Header.Set("Referer", ref) }下面这个注释,调用了一个 process 的函数,但注释对理解 process 函数没有任何作用
// Process every element in the list for e := range elements { process(e) }
解释代码实现的原因
适合解释代码的外部因素或提供额外的上下文。
下面这一段代码,如果没有这个注释,我们可能无法理解 shouldRedirect 为什么要等于 false
switch resp.StatusCode {
// ...
case 307, 308:
redirectMethod = reqMethod
shouldRedirect = true
includeBody = true
if ireq.GetBody == nil && ireq.outgoingLength() != 0 {
// We had a request body, and 307/308 require
// re-sending it, but GetBody is not defined. So just
// return this response to the user instead of an
// error, like we did in Go 1.7 and earlier.
shouldRedirect = false
}
}
// ...
}
解释代码什么情况会出错
适合解释代码的限制条件
下面代码解释了解析字符串时区的流程,并介绍了不合法的字符串会发生什么,怎么处理
// parseTimeZone parses a time zone string and returns its length. Time zones
// are human-generated and unpredictable. We can't do precise error checking.
// On the other hand, for a correct parse there must be a time zone at the
// beginning of the string, so it's almost always true that there's one
// there. We look at the beginning of the string for a run of upper-case letters.
// If there are more than 5, it's an error.
// If there are 4 or 5 and the last is a T, it's a time zone.
// If there are 3, it's a time zone.
// Otherwise, other than special cases, it's not a time zone.
// GMT is special because it can have an hour offset.
func parseTimeZone(value string) (length int, ok bool)
公共符号始终要注释
-
包中声明的每个公共符号(变量、常量、函数以及结构)都要添加注释
下面这个函数的注释,介绍了这个函数的功能以及怎么工作
// 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) -
任何既不明显也不简短的公共功能必须予以注释
-
无论长度或复杂度程度如何,对库中的任何函数都必须进行注释
但也有例外:
-
不需要注释实现接口的方法。例如下面的代码对解释函数的作用毫无意义,只是让我们去参考其他的地方。
//Read implements the io.Reader interface func (r *FileReader) Read(buf []byte) (int, error) -
不要重复注释
第 17 行的函数也是导出的,但紧跟着上面的结构体,上面的代码注释已经解释了这个函数的作用,在这个函数就不需要重复写了。
// LimitReader returns a Reader that reads from r // but stops with EOF after n bytes. // The underlying implementation is a *LimitedReader. func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n, nil} } // A LimitedReader reads from R but limits the amount of // data returned to just N bytes. Each call to Read // updates N to reflect the new amount remaining. // Read returns Err when N <= 0. // If Err is nil, it returns EOF instead. type LimitedReader struct { R Reader // underlying reader N int64 // max bytes remaining Err error // error to return on reaching the limit } func (l *LimitedReader) Read(p []byte) (n int, err error) { if l.N <= 0 { err := l.Err if err == nil { err = EOF } return 0, err } if int64(len(p)) > l.N { p = p[0:l.N] } n, err = l.R.Read(p) l.N -= int64(n) return }
命名规范
Good naming is like a good joke. If you have to explain it, it’s not funny.
——Dave Cheney
variable
-
简洁胜于冗长
-
缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
- ServeHTTP 而不是 ServeHttp
- XMLHTTPRequest 或者 xmlHTTPRequest
-
变量距离其被使用的地方越远,需要携带的上下文信息就越多
全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认其含义
// God
for i := 0; i < len(s); i++ {
// do something
}
// Bad
for index := 0; index < len(s); i++ {
// do something
}
i 和 index 的作用范围仅限于 for 循环内部时,index 的额外冗长几乎没有增加对于程序的理解
对于函数内的参数:
// Good
func (c *Client) send(req *Request, deadline time.Time)
// Bad
func (c *Client) send(req *Request, t time.Time)
将 deadline 替换成 t 降低了变量名的信息量,t 常代指任何时间,deadline 指截止时间,有特殊意义。
function
- 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的
- 函数名要尽量简短
- 当名为 foo 的包某个函数返回类型 Foo 时,可以省略类型信息而不导致歧义
- 当名为 foo 的包某个函数返回类型 T 时(T 并不是 Foo),可以在函数名中加入类型信息
例子:http 包中创建服务的方法如何命名更好?
func Serve(l net.Listener, handler Handler) error
func ServeHTTP(l net.Listener, handler Handler) error
由于这个函数在 http 包中,调用时一般都是 http.xxx ,所以第一种显然是更好的。
package
- 只有小写字母组成,不包含大写字母和下划线等字符
- 简短并包含一定的上下文信息。eg:schema、task 等
- 不要与标准库同名。
以及尽量满足:
- 不使用常用变量名作为包名。eg:bufio 而不是 buf
- 使用单数而不是复数。eg:encoding 而不是 encodings
- 谨慎地使用缩写。eg:format 简写为 fmt
控制流程
线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支,保证正常流程清晰。
-
如果两个分支中都包含 return 语句,则可以去除冗余的 else
// Bad if foo { return x } else { return nil } // Good if foo { return x } return nil这样写的好处是保证了正常的情况只出现在 if 中,对于不正常的,都是返回 nil,同时可以在有其他情况时,直接在 if 后面添加 else 就行
-
尽量保证正常代码路径为最小缩进
优先处理错误情况/特殊情况,尽早返回或继续循环来减少嵌套。
看下面这一段的代码:
func OneFunc() error { err := doSomething() if err == nil { err := doAnotherThing() if err == nil { return nil // normal case } return err } return err }这段代码的正常流程的路径被嵌套在两个 if 条件中,而成功退出的条件是 return nil,需要仔细匹配大括号来找到。
如果后续正常流程需要增加一步操作,调用新的函数,则会再增加一层嵌套。修正后:
func OneFunc() error { if err := doSomething(); err != nil { return err } if err := doAnotherThing(); err != nil { return err } return nil }如果有错,直接返回就好,如果没错,执行正常的逻辑。
错误和异常处理
简单错误
简单错误是指仅出现一次的错误,且在其他地方不需要捕获该错误
对于简单的错误,优先使用 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 关键字来将一个错误关联至错误链中
list, _, err := c.GetBytes(cache.Subkey(a.actionID, "srcfiles"))
if err != nil {
return fmt.Errorf("reading srcfiles list: %w", err)
}
Go1.13 在 errors 中新增了三个新 API 和一个新的 format 关键字,分别是 errors.Is、errors.As 、errors.Unwrap 以及 fmt.Errorf 的 %w。如果项目运行在小于 Go1.13 的版本中,导入 golang.org/x/xerrors 来使用。以下语法均已 Go1.13 作为标准。
错误判定
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
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())
}
}()
// ...
}
小结
error 尽可能提供简明的上下文信息链,方便定位问题。
panic 用于真正异常的情况。
recover 在当前的 goroutine 的被 defer 的函数中生效。