这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记
本次课讲了Go高质量编程,这里总结一下Go编程的编码规范
一、什么样的的代码是高质量的
想要写出良好的代码,首先要了解什么是高质量的代码:
编写的代码能够达到正确可靠、简洁清晰、无性能隐患的目标就能称之为高质量代码
- 各种边界条件是否考虑完备
- 异常情况处理,稳定性保证
- 易读易维护
二、如何编写高质量Go代码
这里从五个方面进行概括总结。
1.代码格式
- 推荐使用gofmt格式化代码
gofmt是Go语言官方提供的工具,能自动格式化GO语言代码为官方统一风格,常见IDE都支持方便的配置。
- goimport
goimport也是Go语言官方提供的工具,实际等于gofmt加上依赖包管理,自动增删依赖的包引用、将依赖包按字母排序并分类。
2.注释
好的代码有很多注释,坏代码需要很多注释
————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)
}
这段代码注释解释了Open这个函数的作用、打开文件的模式、出错的返回信息,不要只告诉读者这个函数用于打开文件,那样其实没有给出足够的信息,对代码可读性没有实质提升。
- 注释应该解释代码如何做的 适合注释实现过程:
// 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)
}
这段代码注释解释了代码片段的实现方法,使读者能更好地理解代码。
- 注释应该解释代码实现的原因 适合解释代码的外部因素,同时提供额外上下文:
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)
这段代码解释了parseTimeZone这个函数会出错的情况和对应的处理,使代码结构清晰明了。
3.命名规范
Good naming is like a good jok.If you have to explain it, it's not funny.
———— Dave Cheney
(1)对于变量
- 简洁胜于冗长
- 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
- 例如使用 ServerHTTP 而不是 ServerHttp
- 使用 XMLHTTPRequest 或者 smlHTTPRequest
- 变量距离其被使用的地方越远,则需要携带越多的上下文信息
- 全局变量在其名字中需要更多的上下文信息,使得在不同地方可轻易辨认出其含义
//Good 更多信息 deadline指截止时间
func (c *Client) send(req *Request, deadline *time.Time)
//Bad t指任意时间,变量名信息量少
func (c *Client) send(req *Request, t *time.Time)
(2)对于函数
- 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的
//http包中创建服务的函数命名
//Good
func Serve(I net.Listener, handler Handler) error
//Bad
func ServeHTTP(I net.Listener, handler Handler) error
- 函数名尽量简短
- 当名为foo的包的某个函数返回类型 Foo 时,可以省略类型信息而不导致歧义
- 当名为foo的包的某个函数返回类型 T 时(T 不是Foo),可以在函数名加入类型信息
(3)对于包
- 只由小写字母组成,不包含大写字母和下划线等字符
- 简短并包含一定的上下文信息。例如
schema,task等 - 不要与标准库同名。例如不要使用
sync或者string
以下规则尽量满足,以标准库为例
- 尽量不要不使用常用变量名作为包名。例如使用
bufio而不是buf - 使用单数而不是复数,例如使用
encoding而不是encodings - 谨慎地使用缩写。例如使用
fmt在不破坏上下文的情况下比format更简短
4.控制流程
- 避免嵌套,保持正常流程清晰
//如果两个分支中都包含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
}
}
return err
}
//Good
func OneFunc() error {
if err := doSomething(); err != nil {
return err
}
if err := doAnotherThing(); err != nil {
return err
}
return nil
}
小结:
- 线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支
- 正常流程代码沿着
- 提升代码可维护性和可读性
- 故障问题大多出现在复杂的条件语句和循环语句中
5.错误和异常处理
简单错误
简单的错误指的是只出现一次的错误,且在其他地方不需要捕获该错误
- 优先使用
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")
}
tips:
Go1.13在errors中新增三个API和一个新的format关键字,分别是errors.ls, errors.As, errors.Unwrap以及fmt.Errorf的%w。如果项目运行在小于Go1.13的版本中,导入golang.org/x/xerrors 来使用
错误判定
- 判定一个错误是否为特定错误,使用
errors.ls - 不同于使用
==,使用该方法可以判定错误链上的所有错误是否含有特定的错误
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
}
- 在错误链上获取特定种类的错误,使用
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
- 如果当前 goroutine 中所有 deferred 函数都不包含 recover 就会造成整个程序崩溃
- 当程序启动阶段发生不可逆转的错误时,可以在 init 或 main 函数中使用 panic
func main() {
// ...
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())
}
}()
// ...
}