4

我在 Go 中犯了 5 个错误

 2 years ago
source link: http://team.jiunile.com//blog/2020/11/go-5-mistakes.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

我在 Go 中犯了 5 个错误

发表于 2020-11-13

| 分类于 golang

犯错

| 0

| 热度: ℃

字数统计: 2,365 字

|

阅读时长 ≈ 10 分钟

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

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

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

1.1 使用引用循环迭代变量

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

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])

结果将是:

Values: 3 3 3
Addresses: 0xc000014188 0xc000014188 0xc000014188

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

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

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])

新的输出:

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

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

list := []int{1, 2, 3}

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

结果将是:

3 3 3

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

1.2 在循环中调用 WaitGroup.Wait

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

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

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 内循环,你可能想委托另一个函数来做这项工作。

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 在没有显式锁或条件变量的情况下进行同步。

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,就会发生默认情况。尽管这种解决方案可能并不总是有效。

...
select {
case ch <- result:
default:
}
...

3 不使用接口

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

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

type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}

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

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

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

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

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

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

4 不好的顺序结构

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

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 字节的连续数据包。需要添加的填充可以通过以下方式计算:

padding = (align - (offset mod align)) mod align
aligned = offset + padding
= offset + ((align - (offset mod align)) mod align)


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 标志。

$ 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 监视对共享变量的不同步访问。

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

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

微信订阅号


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK