《100 Go Mistakes and How to Avoid Them》之控制结构篇

2023-06-01 ⏳1.9分钟(0.7千字)

4.1 忽略元素在range循环中是值拷贝

在Go中 range是一个比较方便的控制循环的方式,可用于:

  1. 字符串

  2. 数组

  3. 指针数组

  4. 切片

  5. Map

  6. 接收的channel

我们必须要理解 range循环中是值传递。

type account struct {
    balance float32
}
accounts := []account{
    {balance: 100.},
    {balance: 200.},
    {balance: 300.},
}
for _, a := range accounts {
    a.balance += 1000
}

以上代码的结果 还是[{100} {200} {300}]。因为我们修改的是一份值拷贝。正确的方式应该是:

for i := range accounts {
    accounts[i].balance += 1000
}

for i := 0; i < len(accounts); i++ {
    accounts[i].balance += 1000
}

accounts := []*account{
    {balance: 100.},
    {balance: 200.},
    {balance: 300.},
}
for _, a := range accounts {
    a.balance += 1000
}

4.2 忽略参数在range循环中计算方式

s := []int{0, 1, 2}
for range s {
    s = append(s, 10)
}

以上代码就陷入死循环么,答案是不会 因为 range的时候 s作为表达式只会执行一次,被copy成为一个临时变量,所以这个slice的length只为3.

s := []int{0, 1, 2}
for i := 0; i < len(s); i++ {
    s = append(s, 10)
}

那这样呢?可以思考一下。这种情况同样适用于其他类型比如channel & array。

4.3 忽略在range循环中使用指针元素的影响

type Customer struct {
    ID      string
    Balance float64
}
type Store struct {
    m map[string]*Customer
}
func (s *Store) storeCustomers(customers []Customer) 
    { for _, customer := range customers {
        s.m[customer.ID] = &customer
    }
}

在上述的例子的,结果是什么

key=1, value=&main.Customer{ID:"3", Balance:0}
key=2, value=&main.Customer{ID:"3", Balance:0}
key=3, value=&main.Customer{ID:"3", Balance:0}

因为我们使用的都是同一个迭代变量,他的内存地址是不变的.map中的*Customer都指向了同一块内存地址

func (s *Store) storeCustomers(customers []Customer) 
    { for _, customer := range customers {
        fmt.Printf("%p\n", &customer)
        s.m[customer.ID] = &customer
    }
}
0xc000096020
0xc000096020
0xc000096020

我们可以用两种主流的方式去规避这个问题,第一种就是Unintended variable shadowing.

func (s *Store) storeCustomers(customers []Customer) { 
    for _, customer := range customers {
        current := customer
        s.m[current.ID] = &current
    }
}

另一种方式就是使用index去寻址:

func (s *Store) storeCustomers(customers []Customer) {
    for i := range customers {
        customer := &customers[i]
        s.m[customer.ID] = customer
    } 
}

4.4 错误理解Map的迭代

Map的Ordering是无序的,与插入顺序无关,且每次都是变化的。至于Go为什么要用这种方式去迭代Map,其实目的就是为了让开发者使用Map的时候不要去依赖顺序(see [this]https://github.com/golang/go/commit/d1f627f2f3f6fc22ed64e1cc7b17eefca952224b). 另一点就是在迭代Map的时候更新map,不会出现编译错误或者运行时错误。但我们也需要考虑去避免死循环的问题,比如:

m := map[int]bool{
    0: true,
    1: false,
    2: true, 
}
for k, v := range m {
    if v {
        m[10+k] = true
    }
}
fmt.Println(m)

以上的代码结果是不可预测的,我们跑了多次的结果:

map[0:true 1:false 2:true 10:true 12:true 20:true 22:true 30:true] 
map[0:true 1:false 2:true 10:true 12:true 20:true 22:true 30:true 32:true] 
map[0:true 1:false 2:true 10:true 12:true 20:true]

我们可以查看Go的规范关于在Map迭代的时候新增.If a map entry is created during iteration, it may be produced during the iteration or skipped. The choice may vary for each entry created and from one iteration to the next. 我们能做的方式就是用一份copy去完成,最后再赋值。

4.5 忽略break语句的作用域

break语句可以结束一个循环。在switchselect中经常会犯错。比如下面想要跳出for循环,可以借助label的特性,有点类似goto.

loop:
    for i := 0; i < 5; i++ {
        fmt.Printf("%d ", i)
        switch i {
            default:
            case 2:
                break loop 
        }
    }

4.6 在循环中使用defer

defer语句会延迟执行一个调用在函数返回之前,经常用于关闭一些资源。但是在循环中使用,经常会犯错。

func readFiles(ch <-chan string) error {
    for path := range ch {
        file, err := os.Open(path)
        if err != nil {
            return err 
        }
        defer file.Close()
        // Do something with file
    }
    return nil 
}

这是一个比较经典的例子。ch堵塞的情况函数不会return,defer永远执行不了,fd无法关系,就会存在内存泄漏。fix的方案有三: 1. 手动close。 这样就摒弃了一些Go的特性。 2. 抽一个readFile函数处理文件。 3. 使用闭包函数。本质就是要让defer在每个迭代中执行。