range是go提供的一种迭代遍历手段,可以遍历数组、切片、Map、channel等,使用频率非常高。

实现原理

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

使用range遍历切片,会生成如下的代码:

for_temp := range
len_temp := len(for_temp)
for index_temp = 0; index_temp < len_temp; index_temp++ {
	value_temp = for_temp[idex_temp]
	index = index_temp
	value = value_temp
	original body
}

遍历slice前先获取slice的长度len_temp作为循环次数,循环体中,每次循环会先获取元素值,如果for-range中接收index和value的话,则会对index和value进行一次赋值。

由于循环开始前循环次数就已经确定了,所以循环过程中新添加的元素是没办法遍历到的,例如:

func main() {
	v := []int{1,2,3}
	for i := range v {
		v = append(v, i)
	}
}

这样的写法是能够正常结束的。

数组和数组指针的遍历过程与slice基本一致。

range for map

遍历map的是过程如下:

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底层实现采用hash表,插入数据的位置是随机的,所以遍历过程中新插入的数据不能保证遍历到

range for channel

遍历channel最为特殊

for {
	value_temp, ok_temp = <-range
	if !ok_temp {
		break
	}
	value = value_temp
	original body
}

channel遍历是依次从channel中读取数据,读取前不知道有多少元素。 如果channel中没有元素,则会阻塞等待,如果channel已被关闭,则会解除阻塞并退出循环。

总结

  • 遍历过程可以视情况放弃接受index或value,可以一定程度上提升性能。
  • 遍历channel时,如果channel中没有数据,可能会阻塞
  • 尽量避免在遍历过程中修改原数据
  • for-range的实现实际上是c风格的for循环
  • 使用index,value接收range返回值会发生一次数据拷贝。