Go新手容易踩的坑(控制结构相关)

1、忽视在range循环中元素被复制的事实

修改结构体切片中的元素

错误的修改方式(要注意:在range循环中,值元素是一个拷贝!

package tests

import (
    "fmt"
    "testing"
)

type Account struct {
    Balance int
}

func TestT1(t *testing.T) {

    accounts := []Account{
        {Balance: 10},
        {Balance: 20},
        {Balance: 30},
    }

    // 1、这样做不会修改
    for _, a := range accounts {
        // 只改变了临时变量a,并没有改变切片中的元素
        a.Balance += 100
    }

    fmt.Println("accounts: ", accounts) // accounts:  [{10} {20} {30}]

}

正确的修改方式1:使用切片的索引访问元素

package tests

import (
    "fmt"
    "testing"
)

type Account struct {
    Balance int
}

func TestT1(t *testing.T) {

    accounts := []Account{
        {Balance: 10},
        {Balance: 20},
        {Balance: 30},
    }
    
    for idx, _ := range accounts {
        accounts[idx].Balance += 100
    }

    fmt.Println("accounts: ", accounts) // accounts:  [{110} {120} {130}]

}

正确的修改方式2:传统的for循环实现,实际上也是通过索引的方式修改

package tests

import (
    "fmt"
    "testing"
)

type Account struct {
    Balance int
}

func TestT1(t *testing.T) {

    accounts := []Account{
        {Balance: 10},
        {Balance: 20},
        {Balance: 30},
    }

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

    fmt.Println("accounts: ", accounts) // accounts:  [{110} {120} {130}]

}

正确的修改方式4:改成访问“切片结构体指针”

但是书中又提到了:“如果性能很重要,由于缺乏可预测性,CPU在指针上的迭代效率可能比较低(书中错误#91中会有讨论)”

package tests

import (
    "fmt"
    "testing"
)

type Account struct {
    Balance int
}

func TestT1(t *testing.T) {

    accounts := []*Account{
        {Balance: 10},
        {Balance: 20},
        {Balance: 30},
    }

    for _, val := range accounts {
        val.Balance += 200
    }

    for _, val := range accounts {
        fmt.Println("val.Balance: ", val.Balance)
    }
    /*
        val.Balance:  210
        val.Balance:  220
        val.Balance:  230
    */
    
}

2、忽视range循环中参数是如何求值的

range循环的语法需要一个表达式:for i, v := range exp。这个exp就是表达式:注意,exp表达式仅求一次值(“求值”的含义是将表达式复制到一个临时变量)!它可以是 字符串、数组、指向数组的指针、切片、map、channel

切片

记住结论:由于range表达式是在循环开始前求值的,所以赋值到循环临时变量的是这个切片的拷贝!

看下面的例子,因为“exp仅求一次值”,所以循环会终止:

package tests

import (
    "fmt"
    "testing"
)

func TestT1(t *testing.T) {

    s := []int{0, 1, 2, 3}
    
    for i, _ := range s { // 将表达式复制到临时变量
        fmt.Println(i)
        s = append(s, 10)
    }

    fmt.Println("s: ", s) // s:  [0 1 2 3 10 10 10 10]

}

为了理解这个问题,我们应该知道当使用一个range循环时,所提供的表达式只计算一次,在循环开始之前。在这个上下文中,“求值”意味着提供的表达式被复制到一个临时变量,然后range迭代这个变量。在本例中,当对s表达式求值时,结果是一个切片副本,如图 4.1 所示。

range循环使用这个临时变量。原始切片s也在每次迭代期间更新。因此,在三次迭代之后,状态如图 4.2 所示。

channel

同样channel跟上面的结论是一样的:

package tests

import (
    "fmt"
    "testing"
)

func TestT1(t *testing.T) {

    ch1 := make(chan int, 3) // 创建第一个channel 里面会包含 0,1,2
    go func() {
        ch1 <- 0
        ch1 <- 1
        ch1 <- 2
        close(ch1)
    }()

    ch2 := make(chan int, 3) // 创建第一个channel 里面会包含 10,11,12
    go func() {
        ch2 <- 10
        ch2 <- 11
        ch2 <- 12
        close(ch2)
    }()

    ch := ch1

    // 通过迭代ch 创建channel消费者
    // 注意:range后面的ch,也是一个“临时变量”
    for v := range ch {
        fmt.Println("v: ", v)
        // 将第二个channel赋值给ch
        ch = ch2
    }
    // 最后打印ch中的元素 变成ch2的了
    for v := range ch {
        fmt.Println("改变后的v: ", v)
    }

    /*
        v:  0
        v:  1
        v:  2
        改变后的v:  10
        改变后的v:  11
        改变后的v:  12
    */

}

数组

由于range表达式是在循环开始前求值的,所以赋值到循环临时变量的是这个数组的拷贝!

案例1:

package tests

import (
    "fmt"
    "testing"
)

func TestT1(t *testing.T) {

    a := [3]int{0, 1, 2}

    for i, v := range a {
        // 修改原数组的值
        a[2] = 10
        // 这里取的是 “拷贝” 的数组,所以里面的元素不会变!
        if i == 2 {
            fmt.Println("v: ", v) // 2
        }
    }

}

访问原数组的值就是最新的了:

package tests

import (
    "fmt"
    "testing"
)

func TestT1(t *testing.T) {

    a := [3]int{0, 1, 2}

    for i, _ := range a {
        // 修改原数组的值
        a[2] = 10
        // 访问原数组的值!被改变了
        if i == 2 {
            fmt.Println("a[2]: ", a[2]) // 10
        }
    }

}

使用“数组指针”,访问的就是原数组,也会被改变的:

package tests

import (
    "fmt"
    "testing"
)

func TestT1(t *testing.T) {

    a := [3]int{0, 1, 2}

    for i, v := range &a {
        // 修改原数组的值
        a[2] = 10
        // 访问原数组的值!被改变了
        if i == 2 {
            fmt.Println("v: ", v) // 10
        }
    }

}

3、忽视在range循环中使用指针元素的影响(注意go1.22版本开始有变化了!)***

修改携带结构体的map的属性的一个经典例子 

map中的元素是结构体:

package tests

import (
    "fmt"
    "testing"
)

type Store struct {
    Name string
}

func updateMapValue(mapValue map[string]Store, id string) {
    val := mapValue[id] // 复制
    val.Name = "bar"

    mapValue[id] = val // 添加 or 修改
}

func TestT1(t *testing.T) {
    
    m1 := map[string]Store{
        "id1": {Name: "id1Name"},
    }

    updateMapValue(m1, "id2")

    fmt.Println("m1: ", m1) // m1:  map[id1:{id1Name} id2:{bar}]

}

map中的元素是结构体指针:

package tests

import (
    "fmt"
    "testing"
)

type Store struct {
    Name string
}

func updateMapValue(mapValue map[string]*Store, id string) {
    // 直接修改
    // Notice 注意先判断 有没有key
    _, ok := mapValue[id]
    if ok {
        mapValue[id].Name = "barbar"
    } else {
        mapValue[id] = &Store{
            Name: "bbssaa",
        }
    }

}

func TestT1(t *testing.T) {

    m1 := map[string]*Store{
        "id1": {Name: "id1Name"},
    }

    updateMapValue(m1, "id2")

    for key, val := range m1 {
        fmt.Println("key: ", key, "val.Name: ", val.Name)
    }
    /*
        key:  id1 val.Name:  id1Name
        key:  id2 val.Name:  bbssaa
    */

}

go1.22之前的“惰性计算”的例子

TODO 下面这个有问题的代码 放在 go1.22 解释器下再运行一下:::::

package tests

import (
    "fmt"
    "testing"
)

type Customer struct {
    ID      string
    Balance float64
}

type Store struct {
    M map[string]*Customer
}

func (s *Store) storeCustomers(customers []Customer) {
    // 这个item:每次只会创建一个固定地址的item
    for _, item := range customers {
        // Notice 打印一下item的地址
        fmt.Printf("item: %p \n", &item)
        // Notice 每次地址一样!
        /*
            item: 0xc0000040d8
            item: 0xc0000040d8
            item: 0xc0000040d8
        */

        s.M[item.ID] = &item
    }
}

func TestT1(t *testing.T) {

    // Notice 注意要这样初始化
    s := Store{
        M: make(map[string]*Customer),
    }

    s.storeCustomers([]Customer{
        {ID: "1", Balance: 10},
        {ID: "2", Balance: 20},
        {ID: "3", Balance: 30},
    })

    for key, val := range s.M {
        fmt.Println("key: ", key, "val: ", val)
    }
    // Notice 惰性计算...
    /*
        key:  1 val:  &{3 30}
        key:  2 val:  &{3 30}
        key:  3 val:  &{3 30}
    */

}

解决方案1:改成切片中存储结构体指针就好了

package tests

import (
    "fmt"
    "testing"
)

type Customer struct {
    ID      string
    Balance float64
}

type Store struct {
    M map[string]*Customer
}

// 切片中存放结构体指针
func (s *Store) storeCustomers(customers []*Customer) {
    // 这样直接赋值就好了
    for _, item := range customers {
        // 打印地址 这次地址都不一样了,因为切片中村的是结构体的指针
        fmt.Printf("item: %p \n", item)
        /*
            item: 0xc0000040d8
            item: 0xc0000040f0
            item: 0xc000004108
        */
        
        s.M[item.ID] = item
    }
}

func TestT1(t *testing.T) {

    // Notice 注意要这样初始化
    s := Store{
        M: make(map[string]*Customer),
    }

    s.storeCustomers([]*Customer{
        {ID: "1", Balance: 10},
        {ID: "2", Balance: 20},
        {ID: "3", Balance: 30},
    })

    for key, val := range s.M {
        fmt.Println("key: ", key, "val: ", val)
    }
    /*
        key:  3 val:  &{3 30}
        key:  1 val:  &{1 10}
        key:  2 val:  &{2 20}
    */

}

解决方案2:局部变量法

package tests

import (
    "fmt"
    "testing"
)

type Customer struct {
    ID      string
    Balance float64
}

type Store struct {
    M map[string]*Customer
}

func (s *Store) storeCustomers(customers []Customer) {
    // 这个item:每次只会创建一个固定地址的item
    for _, item := range customers {
        currItem := item

        s.M[item.ID] = &currItem
    }
}

func TestT1(t *testing.T) {

    // Notice 注意要这样初始化
    s := Store{
        M: make(map[string]*Customer),
    }

    s.storeCustomers([]Customer{
        {ID: "1", Balance: 10},
        {ID: "2", Balance: 20},
        {ID: "3", Balance: 30},
    })

    for key, val := range s.M {
        fmt.Println("key: ", key, "val: ", val)
    }
    /*
        key:  1 val:  &{1 10}
        key:  2 val:  &{2 20}
        key:  3 val:  &{3 30}
    */

}

解决方案3:使用切片的索引存储引用每个元素的指针

package tests

import (
    "fmt"
    "testing"
)

type Customer struct {
    ID      string
    Balance float64
}

type Store struct {
    M map[string]*Customer
}

func (s *Store) storeCustomers(customers []Customer) {

    for idx, _ := range customers {
        // 索引法
        currItem := &customers[idx]
        s.M[currItem.ID] = currItem
    }
}

func TestT1(t *testing.T) {

    // Notice 注意要这样初始化
    s := Store{
        M: make(map[string]*Customer),
    }

    s.storeCustomers([]Customer{
        {ID: "1", Balance: 10},
        {ID: "2", Balance: 20},
        {ID: "3", Balance: 30},
    })

    for key, val := range s.M {
        fmt.Println("key: ", key, "val: ", val)
    }
    /*
        key:  1 val:  &{1 10}
        key:  2 val:  &{2 20}
        key:  3 val:  &{3 30}
    */

}

4、在map迭代过程中做出错误假设

排序

只要记住一个结论就好了:golang中的map的key是“无序的”!

在迭代期间更新map

在迭代期间往map中添加数据

package tests

import (
    "fmt"
    "testing"
)

func TestT1(t *testing.T) {

    m := map[int]bool{
        0: true,
        1: false,
        2: true,
    }

    for key, val := range m {
        if val {
            m[10+key] = true
        }
    }
    // Notice 每次运行的结果不一样!
    fmt.Println("m: ", m)
    // m:  map[0:true 1:false 2:true 10:true 12:true 20:true 22:true 30:true 32:true]
}

Go语言规范中关于迭代时map新键值对的说明:如果在迭代时创建map键值对,它可能在迭代期间被创建(对后面的迭代产生影响),也可能会被跳过,每个键值对的创建对后面的每次迭代产生的影响都可能不同!

解决方案:使用map的拷贝

package tests

import (
    "fmt"
    "testing"
)

func copyMap(originMap map[int]bool) map[int]bool {
    ret := make(map[int]bool)

    if len(originMap) > 0 {
        for key, val := range originMap {
            ret[key] = val
        }
    }

    return ret
}

func TestT1(t *testing.T) {

    m := map[int]bool{
        0: true,
        1: false,
        2: true,
    }

    // Notice 注意不能直接 m2 := m,因为map是引用类型!后面修改m2就相当于修改m了!!!
  // 必须单独创建一个新的m2!!!
    m2 := copyMap(m)

    for key, val := range m {
        m2[key] = val

        if val {
            m2[10+key] = true // 更新m2而不是m
        }

    }

    fmt.Println("m2: ", m2)
    // m2:  map[0:true 1:false 2:true 10:true 12:true]

}

5、忽视break语句是如何工作的

break语句常用来中断循环。当循环与switch或select一起使用的时候,开发者经常执行了错误的break语句。

循环中有switch

不会break掉for循环的情况

package tests

import (
    "fmt"
    "testing"
)

func TestT1(t *testing.T) {

    for i := 0; i < 5; i++ {

        switch i {
        default:
            fmt.Println("default...", i)
        case 2:
            fmt.Println("等于2,但是下面的break并没有break掉for循环!")
            break
        }

    }
    /*
        default... 0
        default... 1
        等于2,但是下面的break并没有break掉for循环!
        default... 3
        default... 4
    */

}

可以使用return,直接终止循环:

package tests

import (
    "fmt"
    "testing"
)

func TestT1(t *testing.T) {

    for i := 0; i < 5; i++ {

        switch i {
        default:
            fmt.Println("default...", i)
        case 2:
            fmt.Println("等于2,return会终止循环!")
            return
        }

    }
    /*
        default... 0
        default... 1
        等于2,return会终止循环!
    */

}

也可以使用loop语句:

package tests

import (
    "fmt"
    "testing"
)

func lop() {
loop:
    for i := 0; i < 5; i++ {

        switch i {
        default:
            fmt.Println("default...", i)
        case 2:
            fmt.Println("等于2,break掉loop后会终止循环!")
            break loop
        }

    }
}

func TestT1(t *testing.T) {

    lop()
    /*
        default... 0
        default... 1
        等于2,return会终止循环!
    */
}

6、在循环中使用defer

我们要实现一个函数:从一个channel中接收文件路径并打开这些文件。因此,我们需要迭代这个channel,打开文件,处理关闭流程。

下面是有问题的代码

package tests

import "os"

func readFiles(ch <-chan string) error {
    
    // Notice ch中没有数据会夯住~~
    for path := range ch {
        file, err := os.Open(path)
        if err != nil {
            return err
        }
        // Notice 这里使用 defer 关闭文件不合适!
        defer file.Close()

        // TODO 对文件进行操作...

    }

    return nil
}

上面的实现有一个很大的问题:因为defer会在所在函数返回时对一个函数进行调用。但是在这个例子中,defer调用不是在每次循环迭代期间执行的!而是在readFiles这个函数返回时执行!如果readFiles函数不返回(别忘了,上面从channel中获取值,如果channel中没有数据代码会夯住...),这些问价描述符将会一直打开,从而导致内存泄漏!

解决方案1:单独写一个打开文件的方法

package tests

import "os"

func readFiles(ch <-chan string) error {

    // Notice ch中没有数据会夯住~~
    for path := range ch {
        if err := readFile(path); err != nil {
            return err
        }
    }

    return nil
}

func readFile(path string) error {
    file, err := os.Open(path)
    if err != nil {
        return err
    }

    // Notice 单独的函数,函数执行结束后会关闭文件
    defer file.Close()

    // TODO 对文件进行操作...

    return nil
}

解决方案2:使readFile函数成为一个闭包(跟方案1其实差不多)

package tests

import (
    "os"
)

func readFiles(ch <-chan string) error {

    // Notice ch中没有数据会夯住~~
    for path := range ch {
        // 闭包
        errB := func(p string) error {
            file, err := os.Open(p)
            if err != nil {
                return err
            }

            // Notice 单独的函数,函数执行结束后会关闭文件
            defer file.Close()

            // TODO 对文件进行操作...

            return nil
        }(path)

        if errB != nil {
            return errB
        }
    }

    return nil
}

~~~

参考

注意一下,个人建议还是买纸质版的书,下面的博客里面因为是机翻的,所以不可避免会有一些看起来很难理解的地方~

https://gomis100.flygon.net/#/docs/03

posted on 2024-07-06 20:23  江湖乄夜雨  阅读(19)  评论(0编辑  收藏  举报