- 遍历切片:下面程序上有没有可优化的空间?
func rangeTest(slice []int) { for index, value := range slice { _, _ = index, value } } 复制代码
解析:使用 range 遍历,每次迭代会对 index,value 进行赋值,若数据很大或 value 类型为 string 时,对 value 的赋值操作可以进行优化,即忽略 value 值,使用 slice[index] 来获取 value 的值。
- 动态遍历:下面程序上能否正常结束?
func main() { v := []int{1,2,3} for i := range v { v = append(v, i) } } 复制代码
解析:会正常结束。循环内再改变切片的长度,不影响循环次数,循环次数在循环开始前就已经是确定了的。
- 遍历Map:下面程序上有没有可优化的空间?
func rangeTest(mapTest map[int]string) { for key, _ := range mapTest { _, _ = key, mapTest[key] } } 复制代码
解析:使用 range 遍历,根据第一题经验,我们根据 key 值来获取value 的值,看似减少了一次赋值,但使用 mapTest[key] 来获取 value 值的性能消耗可能高于赋值消耗。能否优化取决于 map 所存储数据结构特征,应结合实际情况进行。
实现原理
对于 for-range 语句的实现,从编译器源码 gofrontend/go/statements.cc/For_range_statement::do_lower() 方法中可以看到有如下注释:
// Arrange to do a loop appropriate for the type. We will produce // for INIT ; COND ; POST { // ITER_INIT // INDEX = INDEX_TEMP // VALUE = VALUE_TEMP // If there is a value // original statements // } 复制代码
可见 range 是一个 C 风格的循环结构。range 支持数组、数组指针、切片、map 和 channel 类型。
range for slice
注释解释了遍历slice的过程:
// The loop we generate: // for_temp := range // len_temp := len(for_temp) // for index_temp = 0; index_temp < len_temp; index_temp++ { // value_temp = for_temp[index_temp] // index = index_temp // value = value_temp // original body // } 复制代码
遍历slice前会先获取 slice 的长度 len_temp 作为循环次数,循环体中,每次循环会先获取元素值,如果 for-range 中接收 index和 value 的话,则会对 index 和 value 进行一次赋值。数组与数组指针的遍历过程与 slice 基本一致。
由于循环开始前循环次数就已经确定了,所以循环过程中新添加的元素是无法遍历到的。
range for map
// The loop we generate: // var hiter map_iteration_struct // for mapiterinit(type, range, &hiter); hiter.key != nil; mapiternext(&hiter) { // index_temp = *hiter.key // value_temp = *hiter.val // index = index_temp // value = value_temp // original body // } 复制代码
遍历 map 时没有指定循环次数,循环体与遍历 slice 类似。由于 map 底层实现与 slice 不同,map 底层使用 hash 表实现的。
插入数据位置是随机的,所以遍历过程中新插入的数据不能保证遍历到。
range for channel
// The loop we generate: // for { // index_temp, ok_temp = <-range // if !ok_temp { // break // } // index = index_temp // original body // } 复制代码
channel 遍历是依次从 channel 中读取数据,读取前是不知道里面有多少个元素的。如果 channel 中没有元素,则会阻塞等待,如果 channel 已被关闭,则会解除阻塞并退出循环。
注意:
- 上述注释中 index_temp 实际上描述是有误的,应该为 value_temp,因为 index 对于 channel 是没有意义的。
- 使用 for-range 遍历 channel 时只能获取一个返回值。
总结
- 遍历过程中可以适情况丢弃 index 或 value,可以一定程度上提升性能
- 遍历 channel 时,如果 channel 中没有数据,可能会阻塞
- 使用 index,value 接收 range 返回值会发生一次数据拷贝