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