笔记

30.忽略了元素在 for range循环中被复制 (重要)

需要注意,range循环中的值元素是一个复制的副本。因此,如果值是需要改变结构,只会更新副本,而不是元素本身,除非修改的值或字段是指针。在经典for循环或者for range循环中,通过索引访问元素来进行修改。

31.忽略了参数在for range循环中的计算方式 (重要)

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

s := []int{0, 1, 2}
for range s {
		// debug check slice array ptr (bp[0]), 
		// if append happen slicegrow, array ptr change -> new array
		bp := (*[3]uintptr)(unsafe.Pointer(&s1))
		fmt.Printf("0x%x\\n", bp[0])

    s = append(s, 10)
}

notice: 这段代码在原文中分析过程是有误的,但不影响整体结果;range s 之后会发生值拷贝出现copy s, 在append之前,copy s 和 s的array ptr指向同一地址空间;当发生append之后,s 发生来扩容,s的ptr指向了新的地址空间,copy s还是指向原来的地址空间,两者的ptr指向的地址空间已经不同了;

对于ch chan 同理 , range ch 之后会发生值拷贝出现copy ch;对copy ch进行遍历;

对应数组arr […]int{1,2,3} 也是同理,range arr 之后会发生值拷贝出现copy arr; 对copy arr进行遍历, 如果想想改变对应arr的值,可以使用下标访问,或者 range &arr 之后 反生copy 数组引用&arr 指向同一地址空间;

32.忽略了在for range中使用指针元素的影响 (重要)

由于 for _, val := range &val的地址是一个常量,每次迭代指向不同的值,但是地址是同一个,所以在使用&val, 需要特别注意,如果存放&val值,循环迭代完之后,&val指向最后一个元素;如何解决呢, 两个主要的解决方案: 1. 值拷贝,然后赋予地址;2. 直接只用下标对应值的地址;

33.在map迭代期间做出错误的假设 (重要)

一种是错误的有序性假设, map是无序的,每次循环是随机获取key,就是说,插入顺序和读取的顺序不一致,每次循环读取的顺序都不同;应该清楚这些map无序行为,这样代码就不会基于错误的假设;

那么为什么 Go 有这样一种令人惊讶的方式来遍历map呢?这是语言设计者有意识的选择。他们想添加某种形式的随机性,以确保开发人员在使用map时永远不会依赖任何顺序假设(请参阅http://mng.bz/M2JW);

一种是迭代map时插入k/v,在 Go 中,允许在迭代期间更新map(插入或删除元素);它不会导致编译错误或运行时错误。但是,在迭代期间在map中添加条目时,应该考虑这种情况,以避免出现不确定的结果。出现的情况:

在Go中,如果在迭代过程中创建了map条目,则可以在迭代过程中产生或跳过。对于创建的每个条目以及从一个迭代到下一个迭代,选择可能会有所不同。

必须牢记这种行为,以确保代码不会产生不可预测的输出。如果想在迭代map的同时更新map并确保添加的条目不是迭代的一部分,一种解决方案是copy map,迭代map, 在用新的copy map中添加条目;

总而言之,当使用map时,不应该依赖以下内容:

  • 按键排序的数据
  • 保留插入顺序
  • 确定性迭代顺序
  • 在添加元素的同一迭代中生成的元素

记住这些行为应该有助于避免基于错误假设的常见错误。

34.忽略 break 语句是如何工作的

在Go中, 一个基本规则是break语句终止的执行最里面的for, switch, 或select语句。

如何编写代码来打破循环呢? 最惯用的方法是使用标签, 从对应的标签代码块中break; 或者对标签块代码封装成函数,直接return;

35.在循环中使用 defer

使用defer时,必须记住它会在周围函数返回时安排函数调用。因此,defer在循环内调用将堆叠所有调用:它们不会在每次迭代期间执行,例如,如果循环未终止,这可能会导致内存泄漏。解决这个问题最方便的方法是在每次迭代中引入另一个函数来调用。但是,如果性能至关重要,那么一个缺点就是函数调用增加的开销。如果有这样的情况并且想要防止这种开销,应该在循环之前摆脱defer并手动处理延迟调用。

概括

  • 循环中的值元素range是一个副本。因此要改变一个结构,例如,通过其索引或通过经典for循环访问它(除非修改的元素或字段是指针)。
  • 了解传递给运算符的表达式range在循环开始之前仅计算一次可以帮助避免常见错误,例如通道或切片迭代中的低效赋值。
  • 使用局部变量或使用索引访问元素,可以防止在循环内复制指针时出错。
  • 为确保在使用map时可预测输出,请记住map数据结构
    • 不按键排序数据
    • 不保留插入顺序
    • 没有确定的迭代顺序
    • 不保证在迭代期间添加的元素将在本次迭代中生成
  • break语句终止的执行最里面的for, switch, 或select语句,使用标签,可以从对应的标签代码块中break。
  • 提取函数内部的循环逻辑会导致defer在每次迭代结束时执行一条语句。