Bootstrap

Go 语言高质量编程

编写高质量的 Go 代码~

前言:

本次课程简要介绍了高质量编程的定义和原则,分享了代码格式、注释、命名规范、控制流程、错误和异常处理五方面的常见编码规范,帮助我们在今后的开发过程中写出更加优秀的代码 …


什么是高质量编程?

编写的代码能够达到正确可靠、简洁清晰的目标可称之为高质量代码,一份高质量的代码应该具备以下特点:

  • 各种边界条件考虑完备。
  • 异常情况处理,稳定性保证。
  • 易读易维护。
编程原则:

Go 语言开发者 Dave Cheney 给出了三条编程原则,在编程中我们应该尽可能遵循这些原则。

  1. 简单性:消除“多余的复杂性”,以简单清晰的逻辑写代码。
  2. 可读性:编写可维护代码的第一步是确保代码可读。
  3. 生产力:团队整体工作效率非常重要。

编码规范

如何编写高质量的 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:有一个例外,不需要注释实现接口的方法。

image.png

代码格式:

推荐使用 gofmt 自动格式化代码。

gofmt 是 Go 语言官方提供的工具,能自动格式化 Go 语言代码为官方统一风格,常见 IDE 都支持方便的配置。此外 goimports 也是 Go 语言官方提供的工具,可以实现自动增删依赖的包引用、将依赖包按字母序排序并分类。

🎈在 GoLand 中开启 gofmt 支持:

GoLand 提供了 File Watchers 功能,将 go fmt 添加进去,修改触发的条件即可。

image.png

配置完成后每次保存代码时 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 的逆序进行执行。

;