编写高质量的 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 的逆序进行执行。

精彩内容

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: