参考资料:
Review Comments
- 使用 gofmt 或者 goimports 自动格式化代码
- 记录声明的注释应该是完整的句子,注释应以所描述事物的名称开头并以句点结束。
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) { ...
- context.Context 类型的值携带跨 API 和进程边界的安全凭证、跟踪信息、截止日期和取消信号。Go 程序沿着从传入 RPC 和 HTTP 请求到传出请求的整个函数调用链显式传递上下文。大多数使用 Context 的函数都将它作为第一个参数:
1func F(ctx context.Context, /* other arguments */) {}
注意,不要将 Context 成员添加到结构类型中;而是向需要传递该类型的每个方法添加一个 ctx 参数。
- 避免使用
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}
- 以下两种声明切片的方式,前者声明了一个 nil 切片,而后者声明了空切片,两者的 len() 和 cap() 都为 0,通常情况下 nil 切片更好, nil 切片底层指针地址为 0 。
1var t []string
1t := []string{}
2t := make([]string, 0)
例外,在编码 JSON 对象时,推荐使用非 nil 但零长度的切片 ,nil 切片会被编码为 null,而 []string{} 会被编码为 JSON 数组 []。
- 不要使用 panic 进行的错误处理, 而是使用 error 和多返回值。
- 生成协程时,请明确该协程是否结束、何时结束。 Goroutine 可能会因阻塞通道发送或接收而发生泄漏:即使阻塞的通道无法访问,GC 也不会终止 Goroutine。
- 除非出现重名情况,否则避免重命名 import。 大部分情况下的包名称不需要重命名,如果发生冲突,最好重命名本地或特定项目的 import 。
- 尽量使正常的代码路径保持最小的缩进,并缩进错误处理。例如:
不推荐:
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
-
首字母缩写词,具有一致的大小写。 例如,“URL” 应写为"URL"或"url",而不是"Url";使用 “ServeHTTP” 而不是 “ServeHttp”;使用 “xmlHTTPRequest” 或"XMLHTTPRequest";使用 “appID” 而不是 “appId”。由 protocol buffer 生成的代码不受此规则的约束。人类编写的代码比机器编写的代码具有更高的标准。
-
根据实际情况确定函数返回格式。 例如当函数返回相同类型的 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)
-
避免仅仅为了节省几个字节而将指针作为函数参数传递。 如果一个函数自始至终,仅将其参数
x
引用为*x
,那这个传入的参数不应该是指针而是值。将指针作为参数的情况例如:传递指向字符串的指针*string
或指向接口值的指针*io.Reader
,这两个例子中,指针所指向的值本身是固定大小的,因此可以直接传递。但不适用于指针指向大型结构体,包括不适用于可能增长的小型结构体。 -
方法接收器的命名应简短并保持一致:通常用 1 到 2 个字母缩写其类型(“client"缩写为"c"或"cl”)。不要使用通用名称,如 “me”、“this “或 “self”,这些面向对象语言的典型标识符会赋予方法特殊的含义,而在 Go 中,方法的接收器只是另一个参数。
-
通常使用指针接收器,对于基本类型的值或者确定不变的小型结构体可以使用值接收器。
- 如果接收者是一个map、func 或 chan,不要使用指向它们的指针;如果接收者是一个 slice 并且该方法不会重新切片或重新分配该 slice,则不要使用指向它的指针。
- 如果该方法需要改变接收者,则接收者必须是指针。
- 如果接收者是包含sync.Mutex或类似同步字段的结构,则接收者必须是指针,以避免复制。
- 如果接收器是大型结构体或数组,则指针接收器效率更高。
- 相比异步函数,推荐使用同步函数。 同步函数更容易推断函数中协程的生命周期,避免泄漏和数据竞争,也更易于测试:调用者可以传递输入并检查输出,而无需轮询或同步。
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