告诫 Go 初学者,千万别掉进这 5 个坑
共 8863字,需浏览 18分钟
·
2021-04-07 15:06
初学golang我们经常会犯一些错误,虽然它们不会产生类型检查的异常,但是它们往往潜在影响软件的功能。
1. 循环中易犯的错误
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: 0xc0000a4008 0xc0000a4008 0xc0000a4008
你可能会很奇怪为什么会出现这种情况,结果不应该是 1 2 3 和三个不同的地址吗?其实真实原因for range过程中创建了每个元素的副本,而不是直接返回每个元素的引用。v在for循环引进的一个块作用域内进行声明,它是一个共享的可访问的地址。在迭代过程中,返回的变量是根据切片依次赋值的到变量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])
PS:也可以直接根据range返回第一个参数作为数组索引下标 拿值
循环中goroutine使用循环迭代变量也会存在同样的问题:
list := []int{1, 2, 3}
for _, v := range list {
go func() {
fmt.Printf("%d ", v)
}()
}
输出结果:
3 3 3
1.2 循环中调用WaitGroup.Wait
按照WaitGroup的正常用法,当wg.Done()被调用len(tasks)次,wg.Wait()会被自动解除阻塞。当时下面代码中,将wg.wait()放到循环中后,导致第二次循环被阻塞,解决办法 将wg.wait()移除循环即可。
var wg sync.WaitGroup
wg.Add(len(tasks))
for _, t := range tasks {
go func(t *task) {
defer wg.Done()
}(t)
// wg.Wait()
}
wg.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中接收这些数据。默认情况下,发送和接收会阻塞直到对方ready。这使得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函数创建了一个子Goroutine来处理请求,这在go服务端程序中是常见的做法。子Goroutine执行do函数并通过channel发送结果给父节点。子Goroutine将会阻塞直到父节点从channel中收到数据。与此同时,父节点也会阻塞在select上,直到子Goroutine发送结果到channel,或者超时。当超时先发生,则会执行第12行代码并且子Goroutine将永远阻塞。
解决方案:
将ch从无缓冲channel改成有缓冲channel,这样子Goroutine将永远可以发送结果数据,即使父节点已经退出
select中使用default语句,如果没有goroutine收到ch,则会发送默认情况。尽管这种方案不是总能生效。
3. 不使用接口
接口的使用可以让我们的代码更加灵活,也是一种在代码中引入多态的方法。接口允许我们请求一组行为而不是特定类型。不使用接口不会产生任何错误,但是它会导致我们的代码不简洁、不灵活、并且不具备可拓展性。
众多接口中,io.Reader和io.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
}
上面代码看起来两种类型都占用了相同的21bytes的内存空间,但是结果显示却完全不同。我们使用GOARCH=amd64来编译代码:
BadOrderedPerson 类型分配了32bytes
OrderedPerson类型分配了24bytes
为什么会这样呢?原因是数据结构对齐。在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(https://github.com/mdempsky/maligned) 可以轻松检查此类问题。
5. 测试中不使用race detector
数据竞争会引发神秘的错误,经常发生在我们代码部署线上部署很长一段时间后。正是这个原因,它也是并发系统中最常见也是最难调试的问题。为了帮助区分这类bug,Go1.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
-- END --
⬇⬇⬇