参考资料:

  1. https://go.dev/wiki/CodeReviewComments
  2. https://go.dev/wiki/CommonMistakes

Review Comments

  1. 使用 gofmt 或者  goimports 自动格式化代码
  2. 记录声明的注释应该是完整的句子,注释应以所描述事物的名称开头并以句点结束。
1// Request represents a request to run a command.
2type Request struct { ...
3
4// Encode writes the JSON encoding of req to w.
5func Encode(w io.Writer, req *Request) { ...
  1. context.Context 类型的值携带跨 API 和进程边界的安全凭证、跟踪信息、截止日期和取消信号。Go 程序沿着从传入 RPC 和 HTTP 请求到传出请求的整个函数调用链显式传递上下文。大多数使用 Context 的函数都将它作为第一个参数
1func F(ctx context.Context, /* other arguments */) {}

注意,不要将 Context 成员添加到结构类型中;而是向需要传递该类型的每个方法添加一个 ctx 参数。

  1. 避免使用 math/rand 生成密钥。首先,如果不加种子,秘钥结果完全可以预测,即使使用 time.Nanoseconds() 作为种子,生成的秘钥也只有几位不同。推荐使用 crypto/rand 包中的 Read() 生成秘钥。
 1import (
 2    "crypto/rand"
 3    // "encoding/base64"
 4    // "encoding/hex"
 5    "fmt"
 6)
 7
 8func Key() string {
 9    buf := make([]byte, 16)
10    _, err := rand.Read(buf)
11    if err != nil {
12        panic(err)  // out of randomness, should never happen
13    }
14    return fmt.Sprintf("%x", buf)
15    // or hex.EncodeToString(buf)
16    // or base64.StdEncoding.EncodeToString(buf)
17}
  1. 以下两种声明切片的方式,前者声明了一个 nil 切片,而后者声明了空切片,两者的 len() 和 cap() 都为 0,通常情况下 nil 切片更好, nil 切片底层指针地址为 0 。
1var t []string
1t := []string{}
2t := make([]string, 0)

例外,在编码 JSON 对象时,推荐使用非 nil 但零长度的切片 ,nil 切片会被编码为 null,而 []string{} 会被编码为 JSON 数组 []。

  1. 不要使用 panic 进行的错误处理, 而是使用 error 和多返回值。
  2. 生成协程时,请明确该协程是否结束、何时结束。 Goroutine 可能会因阻塞通道发送或接收而发生泄漏:即使阻塞的通道无法访问,GC 也不会终止 Goroutine。
  3. 除非出现重名情况,否则避免重命名 import。 大部分情况下的包名称不需要重命名,如果发生冲突,最好重命名本地或特定项目的 import 。
  4. 尽量使正常的代码路径保持最小的缩进,并缩进错误处理。例如:

不推荐:

1if err != nil {
2    // error handling
3} else {
4    // normal code
5}

推荐:

1if err != nil {
2    // error handling
3    return // or continue, etc.
4}
5// normal code
  1. 首字母缩写词,具有一致的大小写。 例如,“URL” 应写为"URL"或"url",而不是"Url";使用 “ServeHTTP” 而不是 “ServeHttp”;使用 “xmlHTTPRequest” 或"XMLHTTPRequest";使用 “appID” 而不是 “appId”。由 protocol buffer 生成的代码不受此规则的约束。人类编写的代码比机器编写的代码具有更高的标准。

  2. 根据实际情况确定函数返回格式。 例如当函数返回相同类型的 2 个或 3 个参数,或者返回值的含义较难区分,那么推荐在函数返回格式前添加变量名称。

不推荐:

1func (f *Foo) Location() (float64, float64, error)

推荐:

1// Location returns f's latitude and longitude.
2// Negative values mean south and west, respectively.
3func (f *Foo) Location() (lat, long float64, err error)
  1. 避免仅仅为了节省几个字节而将指针作为函数参数传递。 如果一个函数自始至终,仅将其参数 x 引用为 *x,那这个传入的参数不应该是指针而是值。将指针作为参数的情况例如:传递指向字符串的指针 *string 或指向接口值的指针 *io.Reader,这两个例子中,指针所指向的值本身是固定大小的,因此可以直接传递。但不适用于指针指向大型结构体,包括不适用于可能增长的小型结构体。

  2. 方法接收器的命名应简短并保持一致:通常用 1 到 2 个字母缩写其类型(“client"缩写为"c"或"cl”)。不要使用通用名称,如 “me”、“this “或 “self”,这些面向对象语言的典型标识符会赋予方法特殊的含义,而在 Go 中,方法的接收器只是另一个参数。

  3. 通常使用指针接收器,对于基本类型的值或者确定不变的小型结构体可以使用值接收器。

  • 如果接收者是一个map、func 或 chan,不要使用指向它们的指针;如果接收者是一个 slice 并且该方法不会重新切片或重新分配该 slice,则不要使用指向它的指针。
  • 如果该方法需要改变接收者,则接收者必须是指针。
  • 如果接收者是包含sync.Mutex或类似同步字段的结构,则接收者必须是指针,以避免复制。
  • 如果接收器是大型结构体或数组,则指针接收器效率更高。
  1. 相比异步函数,推荐使用同步函数。 同步函数更容易推断函数中协程的生命周期,避免泄漏和数据竞争,也更易于测试:调用者可以传递输入并检查输出,而无需轮询或同步。

Common Mistakes

使用循环变量的引用

Go语言中,循环变量只是一个变量,在每次循环过程中被赋予不同的值。但如果使用不当可能会出现问题。

1func main() {
2	var out []*int
3	for i := 0; i < 3; i++ {
4		out = append(out, &i)
5	}
6	fmt.Println("Values:", *out[0], *out[1], *out[2])
7	fmt.Println("Addresses:", out[0], out[1], out[2])
8}

输出结果为:

1Values: 3 3 3
2Addresses: 0xc0000a0000 0xc0000a0000 0xc0000a0000

原因: 每次循环,Golang 将 i 的地址追加到切片 out 中,但自始至终只有一个 i ,所以循环结束后 i 上的值为最后一次循环赋给 i 的值。

解决方法: 将循环变量赋值到新变量中。

1for i := 0; i < 3; i++ {
2		i := i
3		out = append(out, &i)
4	}

输出结果为:

1Values: 0 1 2
2Addresses: 0xc00001a0b8 0xc00001a0c0 0xc00001a0c8

原因: i := i 将循环变量 i 复制到一个新变量中,该新变量的作用域为 for 循环主体块,也称为 i。每次循环迭代中都会创建这样一个新变量。

虽然这个例子看起来有点明显,但在其他一些情况下,同样的意外行为可能会更加隐蔽。例如,循环变量可以是数组,而引用可以是片段:

上述样例较为明显,实际情况下这种错误可能更加隐蔽,例如,循环变量为数组,其引用为切片。

1func main() {
2	var out [][]int
3	for _, i := range [][1]int{{1}, {2}, {3}} {
4		out = append(out, i[:])
5	}
6	fmt.Println("Values:", out)
7}

输出结果为:

1Values: [[3] [3] [3]]

当在 Goroutine 中使用循环变量时,也会出现同样的问题(下一节)。

在循环变量上使用 goroutines

在 Go 中使用循环时,我们会尝试使用 goroutine 并行处理数据。例如可以使用闭包:

 1func main() {
 2	values := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
 3	for _, val := range values {
 4		go func() {
 5			fmt.Println(val)
 6		}()
 7	}
 8
 9	// wait 10 second
10	time.Sleep(10 * time.Second)
11}

上述代码不会按顺序打印的每个值,因为所有闭包函数都绑定了同一个变量,运行此代码时,因为 goroutine 可能要等到循环结束后才会开始执行,所以只会打印最后一个元素10。

 1$ go run test.go 
 210
 310
 410
 510
 610
 710
 810
 910
1010
1110

编写闭包循环的正确方式是:

 1func main() {
 2	values := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
 3	for _, val := range values {
 4		go func(v int) {
 5			fmt.Println(v)
 6		}(val)
 7	}
 8
 9	// wait for goroutines to finish
10	var input string
11	fmt.Scanln(&input)
12
13}

val 作为参数添加到闭包中,每次迭代时对 val 进行求值,并将其置于 goroutine 的堆栈中,因此每个切片元素在最终执行时可供 goroutine 使用。

 1$ go run test.go 
 24
 31
 42
 53
 67
 75
 86
 98
109
1110

下列代码使用公共索引变量 i 创建单独的 val,也可以产生我们想要的结果:

 1func main() {
 2	values := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
 3	for i := range values {
 4		val := values[i]
 5		go func() {
 6			fmt.Println(val)
 7		}()
 8	}
 9
10	// wait for goroutines to finish
11	var input string
12	fmt.Scanln(&input)
13
14}
 14
 21
 32
 43
 57
 65
 76
 88
 99
1010

注意,如果不将此闭包作为 goroutine 执行,代码将按预期运行。以下示例打印 1 到 10 之间的整数。

1for i := 1; i <= 10; i++ {
2	func() {
3		fmt.Println(i)
4	}()
5}
 11
 22
 33
 44
 55
 66
 77
 88
 99
1010

其中,闭包函数会在变量i更改之前执行完毕,因此最终按顺序输出1到10。 https://go.dev/doc/faq#closures_and_goroutines