Golang 中不要犯这 5 个错误

开发 后端
本文总结了 5 个常见的错误,你检验下自己犯过没有?!这些是我写 Go 时所犯的错误,希望对你有帮助!

[[431845]]

大家好,我是程序员幽鬼。

Go 给人的印象是容易入门,因为语法简单。不过新手还是比较容易犯一些错误的。

本文总结了 5 个常见的错误,你检验下自己犯过没有?!这些是我写 Go 时所犯的错误,希望对你有帮助!

1、循环内部

在循环中,有几种情况可能会导致混乱,你需要弄清楚。

1.1、循环迭代器变量中使用引用

出于效率考虑,经常使用单个变量来循环迭代器。由于在每次循环迭代中会有不同的值,有些时候这会导致未知的行为。例如:

  1. in := []int{1, 2, 3} 
  2.  
  3. var out []*int 
  4. for  _, v := range in { 
  5.  out = append(out, &v) 
  6.  
  7. fmt.Println("Values:", *out[0], *out[1], *out[2]) 
  8. fmt.Println("Addresses:"out[0], out[1], out[2]) 

输出结果:

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

是不是很惊讶?在 out 这个 slice 中的元素都是 3。实际上很容易解释为什么会这样:在每次迭代中,我们都将 v append 到 out 切片中。因为 v 是单个变量(内存地址不变),每次迭代都采用新值。在输出的第二行证明了地址是相同的,并且它们都指向相同的值。

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

  1. in := []int{1, 2, 3} 
  2.  
  3. var out []*int 
  4. for  _, v := range in { 
  5.  v := v 
  6.  out = append(out, &v) 
  7.  
  8. fmt.Println("Values:", *out[0], *out[1], *out[2]) 
  9. fmt.Println("Addresses:"out[0], out[1], out[2]) 

新的输出:

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

在 goroutine 中使用循环迭代变量会有相同的问题。

  1. list := []int{1, 2, 3} 
  2.  
  3. for _, v := range list { 
  4.  go func() { 
  5.   fmt.Printf("%d ", v) 
  6.  }() 

输出将是:

  1. 3 3 3 

可以使用上述完全相同的解决方案进行修复。请注意,如果不使用 goroutine 运行该函数,则代码将按预期运行。

这个错误犯错率是很高的,要特别注意!!

1.2、在循环中调用 WaitGroup.Wait

看一段代码:

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

WaitGroup 常用来等待多个 goroutine 运行完成。但如果 Wait 在循环内部调用,即代码中第 7 行的位置,得到的结果就不是预期的了。这个错误犯错率应该比较低。

1.3、循环内使用 defer

因为 defer 的执行时机是函数返回前。所以,一般不应该在循环内部使用 defer,除非你很清楚自己在干什么。

看一段代码:

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

在上面的示例中,如果使用第 8 行而不是第 10 行,则下一次迭代将无法获得互斥锁,因为该锁并没有释放,所以循环会永远阻塞。

如果你确实需要在循环内部使用 defer,则通过委托给另外一个函数的方式进行:

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

2、channel 堵塞

一般认为 goroutine + channel 是 Go 的利器。Go 强调不要通过共享内存来通讯,而是通过通讯来共享内存。

但在使用 channel 的过程中,需要注意堵塞问题,避免导致 goroutine 泄露。比如下面的代码:

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

检查一下上面的代码的 doReq 函数,在第 4 行创建一个子 goroutine 来处理请求,这是 Go 服务器程序中的常见做法。

子 goroutine 执行 do 函数并通过第 6 行的通道 ch 将结果发送回父 goroutine。子 goroutine 将在第 6 行阻塞,直到父 goroutine 在第 9 行从 ch 接收到结果为止。同时,父 goroutine 将在 select 阻塞,直到子 goroutine 将结果发送给 ch(第 9 行)或超时(第 11 行)。如果超时先发生,则父 goroutine 将从 doReq 第 12 行返回,这会导致没有 goroutine 从 ch 读取数据,子 goroutine 就会一直堵塞在第 6 行。解决办法是将 ch 从无缓冲的通道改为有缓冲的通道,因此子goroutine 即使在父 goroutine 退出后也始终可以发送结果。

这个错误出现概率不会低。还有特别要注意的一点,就是 time.After 导致的内存泄露问题,只要注意程序不是频繁执行上面的 select 即可(毕竟 time.After 到时间了还是会回收资源的)。

3、不使用接口

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

在 Go 接口中,io.Reader 和 io.Writer 可能是使用最多的。

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

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

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

如果第二天,你想写入 http.ResponseWriter,显然不太适合再创建另外一个 Save 方法,这时应该用 io.Writer:

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

另外,你应该知道的重要注意事项是,始终关注行为。在上面的示例中,虽然 io.ReadWriteCloser 也可以使用,但你只需要 Write 方法。接口越大,抽象性越弱。在 Go 中,通常提倡小接口。

所以,我们应该优先考虑使用接口,而不是具体类型。

4、不注意结构体字段顺序

这个问题不会导致程序错误,但是可能会占用更多内存。

看一个例子:

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

看起来这两个类型都占用的空间都是 21字节,但是结果却不是这样。我们使用 GOARCH=amd64 编译代码,发现 BadOrderedPerson 类型占用 32 个字节,而 OrderedPerson 类型只占用 24 个字节。为什么?原因是数据结构对齐[1]。在 64 位体系结构中,内存分配连续的 8 字节数据。需要添加的填充可以通过以下方式计算:

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

当你使用大型常用类型时,可能会导致性能问题。但是不用担心,你不必手动处理所有结构。这工具可以轻松的解决此类问题:https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/fieldalignment。

5、测试中不使用 race 探测器

数据争用会导致莫名的故障,通常是在代码已部署到线上很久之后才出现。因此,它们是并发系统中最常见且最难调试的错误类型。为了帮助区分此类错误,Go 1.1 引入了内置的数据争用检测器(race detector)。可以简单地添加 -race flag 来使用。

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

启用数据争用检测器后,编译器将记录在代码中何时以及如何访问内存,而 runtime 监控对共享变量的非同步访问。

找到数据竞争后,竞争检测器将打印一份报告,其中包含用于冲突访问的堆栈跟踪。这是一个例子:

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

总结

错误不可怕,但我们需要从错误中吸取教训,避免再次掉入同样的坑里。掉入一个坑, 我们应该想办法探究出原因,知道为什么,下次再掉坑的可能性就会小很多。

原文地址:https://hackernoon.com/dont-make-these-5-golang-mistakes-3l3x3wcw

参考资料

[1]数据结构对齐: https://en.wikipedia.org/wiki/Data_structure_alignment

 

责任编辑:武晓燕 来源: 幽鬼
相关推荐

2018-03-15 10:21:50

程序员面试低级错误

2019-07-08 13:58:03

Java数据结构设计

2010-04-20 14:59:56

面试

2017-10-18 10:37:07

VMware虚拟化架构

2019-08-02 16:15:13

2020-07-26 00:40:48

JavaScript开发代码

2020-03-31 22:09:01

React应用程序

2021-02-06 10:27:51

Linux发行版操作系统

2021-02-03 13:03:00

编程程序员语言

2023-11-30 07:15:36

GolangRecover

2023-10-28 16:30:19

Golang开发

2021-12-16 06:52:33

Ceph分布式对象

2019-11-28 18:51:07

PythonPHP编程语言

2020-08-17 17:22:34

VSCode插件开发编码

2020-11-20 10:30:48

云计算SaaS技术

2021-08-06 09:20:41

IT管理IT领导者CIO

2015-06-08 10:31:30

程序员代码

2016-05-04 09:45:01

CSS开发不要

2019-08-01 12:36:54

物联网项目物联网IOT

2019-11-29 10:03:43

5G技术智能家居
点赞
收藏

51CTO技术栈公众号