我在 Go 中犯了 5 个错误

人皆犯错,宽恕是德 — Alexander Pope

这些都是我在写 Go 中犯的错误。尽管这些可能不会导致任何类型的错误,但它们可能会潜在地影响软件。

1 内循环

有几种方法可以造成循环内部的混乱,你需要注意。

1.1 使用引用循环迭代变量

由于效率的原因,循环迭代变量是单个变量,在每次循环迭代中采用不同的值。这可能会导致不知情的行为。

1
2
3
4
5
6
7
8
9
in := []int{1, 2, 3}

var out []*int
for _, v := range in {
out = append(out, &v)
}

fmt.Println("Values:", *out[0], *out[1], *out[2])
fmt.Println("Addresses:", out[0], out[1], out[2])

结果将是:

1
2
Values: 3 3 3
Addresses: 0xc000014188 0xc000014188 0xc000014188

正如你所看到的,out 切片中的所有元素都是 3。实际上,实际上很容易解释为什么会发生这种情况:在每次迭代中,我们都会将 v 的地址附加到 out 切片中。如前所述,v 是在每次迭代中接受新值的单个变量。因此,正如您在输出的第二行中看到的,地址是相同的,并且所有地址都指向相同的值。

简单的解决方法是将循环迭代器变量复制到新变量中:

1
2
3
4
5
6
7
8
9
10
in := []int{1, 2, 3}

var out []*int
for _, v := range in {
v := v
out = append(out, &v)
}

fmt.Println("Values:", *out[0], *out[1], *out[2])
fmt.Println("Addresses:", out[0], out[1], out[2])

新的输出:

1
2
Values: 1 2 3
Addresses: 0xc0000b6010 0xc0000b6018 0xc0000b6020

同样的问题可以找到正在 Goroutine 中使用的循环迭代变量。

1
2
3
4
5
6
7
list := []int{1, 2, 3}

for _, v := range list {
go func() {
fmt.Printf("%d ", v)
}()
}

结果将是:

1
3 3 3

它可以使用上面提到的相同的解决方案来修复。注意,如果不使用 Goroutine 运行该函数,代码将按照预期运行。

1.2 在循环中调用 WaitGroup.Wait

使用 WaitGroup 类型的共享变量会犯此错误,如下面的代码所示,当第 5 行的 Done() 被调用 len(tasks) 次数时,第 7 行的 Wait() 只能被解除阻塞,因为它被用作参数在第 2 行调用 Add()。但是,Wait() 在循环中被调用,因此在下一个迭代中,它会阻止在第 4 行创建 Goroutine。简单的解决方案是将 Wait() 的调用移出循环。

1
2
3
4
5
6
7
8
9
10
var wg sync.WaitGroup
wg.Add(len(tasks))
for _, t := range tasks {
go func(t *task) {
defer group.Done()
}(t)
// group.Wait()
}

group.Wait()

1.3 在循环中使用 defer

defer 直到函数返回才执行。除非你确定你在做什么,否则你不应该在循环中使用 defer

1
2
3
4
5
6
7
8
9
10
11
var mutex sync.Mutex
type Person struct {
Age int
}
persons := make([]Person, 10)
for _, p := range persons {
mutex.Lock()
// defer mutex.Unlock()
p.Age = 13
mutex.Unlock()
}

在上面的例子中,如果你使用第 8 行而不是第 10 行,下一次迭代就不能持有互斥锁,因为锁已经在使用中,并且循环永远阻塞。

如果你真的需要使用 defer 内循环,你可能想委托另一个函数来做这项工作。

1
2
3
4
5
6
7
8
9
10
11
12
var mutex sync.Mutex
type Person struct {
Age int
}
persons := make([]Person, 10)
for _, p := range persons {
func() {
mutex.Lock()
defer mutex.Unlock()
p.Age = 13
}()
}

但是,有时使用 defer 在循环可能会变得很方便。所以你真的需要知道你在做什么。

Go 不能容忍愚蠢者

2 发送到一个无保证的 channel

您可以将值从一个 Goroutine 发送到 channels,并将这些值接收到另一个 Goroutine。默认情况下,发送和接收,直到另一方准备好。这允许 Goroutines 在没有显式锁或条件变量的情况下进行同步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func doReq(timeout time.Duration) obj {
// ch :=make(chan obj)
ch := make(chan obj, 1)
go func() {
obj := do()
ch <- result
} ()
select {
case result = <- ch :
return result
case<- time.After(timeout):
return nil
}
}

让我们检查一下上面的代码。doReq 函数在第 4 行创建一个子 Goroutine 来处理请求,这在Go服务程序中是一种常见的做法。子 Goroutine 执行 do 函数并通过第 6 行通道 ch 将结果发送回父节点。子进程会在第 6 行阻塞,直到父进程在第 9 行接收到 ch 的结果。同时,父进程将阻塞 select,直到子进程将结果发送给 ch(第9行)或发生超时(第11行)。如果超时发生在更早的时候,父函数将从第 12 行 doReq 方法返回,并且没有人可以再接收 ch 的结果,这将导致子函数永远被阻塞。解决方案是将 ch 从无缓冲通道更改为缓冲通道,这样即使父及退出,子 Goroutine 也始终可以发送结果。另一个修复方法是在第 6 行使用默认为空的 select 语句,这样如果没有 Goroutine 接收 ch,就会发生默认情况。尽管这种解决方案可能并不总是有效。

1
2
3
4
5
6
...
select {
case ch <- result:
default:
}
...

3 不使用接口

接口可以使代码更加灵活。这是在代码中引入多态的一种方法。接口允许您请求一组行为,而不是特定类型。不使用接口可能不会导致任何错误,但它会导致代码不简单、不灵活和不具有可扩展性。

在众多接口中,io.Readerio.Writer 可能是最受欢迎的。

1
2
3
4
5
6
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}

这些接口可以非常强大。假设您要将对象写入文件中,因此您定义了一个 Save 方法:

1
func (o *obj) Save(file os.File) error

如果您第二天需要写入 http.ResponseWriter 该怎么办?您不想定义新方法。是吧?所以使用 io.Writer

1
func (o *obj) Save(w io.Writer) error

还有一个重要的注意事项,你应该知道,总是要求你要使用的行为。在上面的例子中,请求一个io.ReadWriteCloser 也可以工作得很好,但当你要使用的唯一方法是 Write 时,这不是一个最佳实践。接口越大,抽象就越弱。

所以大多数时候你最好专注于行为而不是具体的类型。

4 不好的顺序结构

这个错误也不会导致任何错误,但是它会导致更多的内存使用。

1
2
3
4
5
6
7
8
9
10
11
type BadOrderedPerson struct {
Veteran bool // 1 byte
Name string // 16 byte
Age int32 // 4 byte
}

type OrderedPerson struct {
Name string
Age int32
Veteran bool
}

似乎两种类型的大小都相同,为 21 个字节,但结果显示出完全不同。使用 GOARCH=amd64 编译代码,BadOrderedPerson 类型分配 32 字节,而 OrderedPerson 类型分配 24 字节。为什么?原因是数据结构对齐。在 64 位体系结构中,内存分配 8 字节的连续数据包。需要添加的填充可以通过以下方式计算:

1
2
3
padding = (align - (offset mod align)) mod align
aligned = offset + padding
= offset + ((align - (offset mod align)) mod align)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type BadOrderedPerson struct {
Veteran bool // 1 byte
_ [7]byte // 7 byte: padding for alignment
Name string // 16 byte
Age int32 // 4 byte
_ struct{} // to prevent unkeyed literals
// zero sized values, like struct{} and [0]byte occurring at
// the end of a structure are assumed to have a size of one byte.
// so padding also will be addedd here as well.

}

type OrderedPerson struct {
Name string
Age int32
Veteran bool
_ struct{}
}

当您有一个大的常用类型时,它可能会导致性能问题。但是不要担心,您不必手动处理所有的结构。使用 maligned 你可以轻松检查代码以解决此问题。

5 在测试中没有使用 race detector

数据竞争会导致神秘的故障,通常是在代码部署到生产环境很久之后。正因为如此,它们是并发系统中最常见也是最难调试的 bug 类型。为了帮助区分这些 bug, Go 1.1 引入了一个内置的数据竞争检测器。它可以简单地添加 -race 标志。

1
2
3
4
$ go test -race pkg    // to test the package
$ go run -race pkg.go // to run the source file
$ go build -race // to build the package
$ go install -race pkg // to install the package

启用 race 检测器后,编译器将记录在代码中访问内存的时间和方式,而 runtime 监视对共享变量的不同步访问。

当发现数据竞争时,竞争检测器将打印一份报告,其中包含冲突访问的堆栈跟踪。下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
WARNING: DATA RACE
Read by goroutine 185:
net.(*pollServer).AddFD()
src/net/fd_unix.go:89 +0x398
net.(*pollServer).WaitWrite()
src/net/fd_unix.go:247 +0x45
net.(*netFD).Write()
src/net/fd_unix.go:540 +0x4d4
net.(*conn).Write()
src/net/net.go:129 +0x101
net.func·060()
src/net/timeout_test.go:603 +0xaf
Previous write by goroutine 184:
net.setWriteDeadline()
src/net/sockopt_posix.go:135 +0xdf
net.setDeadline()
src/net/sockopt_posix.go:144 +0x9c
net.(*conn).SetDeadline()
src/net/net.go:161 +0xe3
net.func·061()
src/net/timeout_test.go:616 +0x3ed
Goroutine 185 (running) created at:
net.func·061()
src/net/timeout_test.go:609 +0x288
Goroutine 184 (running) created at:
net.TestProlongTimeout()
src/net/timeout_test.go:618 +0x298
testing.tRunner()
src/testing/testing.go:301 +0xe8

6 最后一句

唯一真正的错误是我们什么也没学到。

译自:https://medium.com/swlh/5-mistakes-ive-made-in-go-75fb64b943b8

微信订阅号

-------------本文结束感谢您的阅读-------------

本文标题:我在 Go 中犯了 5 个错误

文章作者:icyboy

发布时间:2020年11月13日 - 22:00

最后更新:2020年11月13日 - 12:58

原始链接:http://team.jiunile.com/blog/2020/11/go-5-mistakes.html

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。