Go语言编码规范 | 青训营笔记

171 阅读7分钟

49928ef3e3054f3cb43848a0e4d577ed_tplv-k3u1fbpfcp-zoom-crop-mark_1304_1304_1304_734.webp 这是我参与「第三届青训营 -后端场」笔记创作活动的的第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
}

小结

  1. 线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支
  2. 正常流程代码沿着
  3. 提升代码可维护性和可读性
  4. 故障问题大多出现在复杂的条件语句和循环语句中

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())
		}
	}()
	// ...
}