「Golang」for range 使用方法及避坑指南

前言

循环控制结构是一种在各种编程语言中常用的程序控制结构,其与顺序控制结构、选择控制结构组成了程序的控制结构,程序控制结构是指以某种顺序执行的一系列动作,用于解决某个问题。理论和实践证明,无论多复杂的算法均可通过顺序、选择、循环3种基本控制结构构造出来。

在Go中,提供了两种循环控制结构forgoto,但是后者不推荐使用(原因请查看艾兹格·迪杰斯特拉(Edsger Wybe Dijkstra)在1968年的一篇名称为《GOTO语句有害论》的论文),但是就作者而言goto在某些业务情况下,是很好用的,所以也不需要完全就反对他。

本文代码基于Go 1.16版本,不同版本如有差异请见谅

万能的for循环

在Go中,与c语言(及大部分语言)不同的是,去掉了while,do..while 循环,将其完全简化为for,虽然这样看起来是缺少了很多功能,但是while,do..while 等功能完全可以通过for来实现 。

接下来,通过几个代码来展示出,for循环,如何实现while,do..while 的相关逻辑。

func main() {
	N := 10
	for i := 0; i < N; i++ {
		// TODO
	}

	for {
		/*
		*   while true{
		*	  // TODO
		*	}
		*
		*/
	}

	for N > 10 {
		/*
		*   while N>10 {
		*	  // TODO
		*	}
		*
		*/
	}
	for {
		// TODO
		// do{
		//  TODO
		// }while(N>10)
    if N <= 10 {
			break
		}
	}
}

代码有些粗糙,望大家见谅,可以看得出来,一个for就可以完成while,do..while 所有的功能,从而看出Go的循环控制结构是多么的强大。

for i := 0; i < N; i++ {}麻烦?来看看语法糖

在我们的Go编写的业务逻辑中,常用的循环方式,为经典的三段式循环,即for i := 0; i < N; i++ {},这种循环可以帮我们方便的遍历数组,切片等数据结构,还可以轻松的进行一定次数循环的操作,那么当我们想要遍历mapchannel时,该如何呢?Go给我们提供了一个新关键字range来进行遍历,可以把它理解为一个三段式循环的语法糖,它不光可以遍历mapchannel,同样的也可以遍历数组,切片等数据结构,但是与传统循环不同的是,他不可以进行普通的次数循环,那么接下来我们来看一下其遍历数组,切片,mapchannel的相关操作以及所能碰到的坑。

遍历数组和切片

遍历数组和切片的方式都是一样的,因为切片的使用概率要大于数组,所以主要讲的切片的遍历,数组可以与其相同方式进行使用,首先 Show me code!

func main() {
	slice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	// first 
	for range slice {
		fmt.Println()
	}
	// second
	for k := range slice {
		fmt.Println(k)
	}
	// third
	for k, v := range slice {
		fmt.Println(k, v)
	}
}

接下来我们挨个的分析一下first,second,third三个注释下面的三种循环方式的区别以及一些特点:

  1. 使用first方式遍历数组和切片,因其range前没有接收变量,因此代表此次循环并不在意返回的索引以及数据,只关心循环次数。
  2. second方式遍历数组和切片,因其range前仅有一个接收变量,因此代表此次循环仅关心返回的索引,不关心返回的数据,此代码等同于for k,_ := range slice
  3. third方式遍历数组和切片,该方法是range的完全体使用形式,因此代表此次循环及关心返回的索引,也关心返回的数据。

三种形式,每种形式都有不同的编译编译器优化后的代码,因为range算是一个语法糖,最终其都会在编译期间优化为传统的三段式循环,那么接下来,来看一下这三种形式的编译期间被转换的代码。

下面的代码信息全部引用自:go/src/cmd/compile/internal/gc/range.go

优化后代码引自:https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-for-range/

首先提前说一下下面会出现的所有变量名的含义,该含义全部引用自编译器源代码。

v1,v2 =>代表 range前变量的索引、数据字段即

for k,v:=range slice{}中的k、v

ha => 被遍历元素的复制版

for k,v:=range slice {}中 slice的复制品

a => 可以理解为被遍历元素

for k,v:=range slice {}中 slice的复制品

即 ha 为 a 的复制版

hn => len(slice) ,即slice的长度

hv1 => 循环内的循环指针,可以理解为

for i := 0; i < N; i++ {}中的 i

忽略索引和数据

首先来看range前没有接收变量、此次循环并不在意返回的索引以及数据,只关心循环次数的循环方式,会产生什么样的译编译器优化后的代码:

ha := a
hv1 := 0
hn := len(ha)
v1 := hv1
for ; hv1 < hn; hv1++ {
    ...
}

由上述代码可以看出,在使用range循环的过程中,原数据被拷贝了一份,然后随后的三段式循环过程完全是围绕着这个复制版本进行操作。

忽略索引和数据就已经讲完了,没什么可说的,很简单,但需要注意的是,range循环被编译器优化改变后,是采用原数据的复制版本进行循环操作的,而不是直接使用原数据进行循环操作,因此,对于切片和数组来说,某些可能修改原数据但不会修改复制版本的操作,不会对每次循环的数据产生影响(还没进行测试,理论上是的)。

只关心索引

其次来看一下range前仅有一个接收变量,因此代表此次循环仅关心返回的索引,不关心返回的数据,会产生什么样的编译器优化后的代码:

ha := a
hv1 := 0
hn := len(ha)
v1 := hv1
for ; hv1 < hn; hv1++ {
    v1 = hv1
    ...
}

由上述代码可以看出,使用了一个变量v1作为接受索引数据的变量,与其对应的是for k := range slice中的k,之所以没有使用hv1直接作为索引变量的原因,个人猜测是怕再循环过程中误修改循环指针的值,即hv1的值,而产生一些不明来源的问题。

从上面可以看出,range的每次循环都是针对一个变量进行循环的赋值,而不是每次循环重新申请内存空间,此处是一个很常见的出现面试题以及出现坑的地方,具体的内容一会会进行详解。

关心返回的索引和数据

最后来看一下range的完全体使用形式,此次循环及关心返回的索引,也关心返回的数据。因此其代码相对上一个也更复杂,接下来看一下会产生什么样的译编译器优化后的代码:

ha := a
hv1 := 0
hn := len(ha)
v1 := hv1
v2 := nil
for ; hv1 < hn; hv1++ {
    tmp := ha[hv1]
    v1, v2 = hv1, tmp
    ...
}

上述代码可见,v2是数据对应的变量,与其对应的是for k,v:= range slice中的v,v1则对应的是k,跟只关心索引的循环一样,每次循环都是针对一个变量进行循环的赋值,而不是每次循环重新申请内存空间。但是在循环中,出现的tmp变量是循环申请的,这是为什么?我个人理解是为了防止切片的元素类型是个指针,如果是个指针的话,直接赋值给v2,随后如果用户在循环过程中对其进行修改就会影响到原切片的复制版本的底层数据,这样是不好的行为(个人猜测,如果有更好的解释可以留言告知于我,在此表示感谢)。

遍历字符串

我们还可以使用range来遍历字符串,使用方式与数组和切片相同,但是有一个地方需要拿出来讲一下,例子代码如下:

func main() {
	str:= "GopherEcho这是我的公众号"
	for k, v := range str {
		fmt.Print(k, ":", string(v), "  ")
	}
  // out:
  // 0:G  1:o  2:p  3:h  4:e  5:r  6:E  7:c  8:h  9:o  10:这  13:是  16:我  19:的  22:公  25:众  28:号  
}

在k<10之前输出都很正常,为什么当k==10的时候每次间隔都为3了呢?这需要我们仍然从编译器优化后的代码的代码来看起,遍历字符串的时候,编译器依旧会将range修改为普通的三段式遍历形式,那么其被修改成什么了呢?接下来看代码:

下列代码摘自:go/src/cmd/compile/internal/gc/range.go 362-375行

	ha := a
	for hv1 := 0; hv1 < len(ha); {
		hv1t := hv1
		hv2 := rune(ha[hv1])
		if hv2 < utf8.RuneSelf {
			hv1++
		} else {
			hv2, hv1 = decoderune(ha, hv1)
		}
		v1, v2 = hv1t, hv2
 	//	todo 
  }

接下来我来解释一下这段代码,首先仍然和遍历数组和切片一样,会对原对象进行一次拷贝,接下来开始使用传统三段式遍历方式进行遍历,接下来看第4行,其把当前遍历到的字节(字符串底层类似为byte数组)转换为rune类型,然后判断该rune类型是否为utf8码点,如果第5行判断为true,则代表当前的rune是一个ASCII字符,此时索引仅+1即可,如果为false,需要将其转换为对应长度的rune字符并且对应索引也会随之增加,decoderune(ha, hv1)这个函数,在go/src/runtime/utf8.go的第60行,感兴趣的可以去看看,在此就不做赘述了,随后,赋值给v1,v1即for k, v := range str的k和v。至此,字符串的遍历也讲完了,没啥可说的主要就是针对utf8码点进行了索引递增,每个中文字符占用3个byte长度。

decoderune函数的功能就是返回 s[k:]开头的非 ASCII 符文和 s 中符文后的索引。其中,s为传入的字符串,k为索引

遍历Map

在之前的文章里面,我进行了Map的源代码的解析,深入的解析了针对Map的读写以及扩容等源代码,接下来讲解一下通过使用range来遍历Map(说的像别的方式可以遍历一样😠)首先看一下遍历Map的方式:

func main() {
	m := map[int]int{
		1: 2,
		3: 4,
		4: 5,
	}
	for k, v := range m {
		fmt.Println(k, v)
	}
}

跟字符串啥的使用方式没啥区别,都是那玩意儿,不用太在意,我们主要看的是编译器修改后的代码。

遍历Map跟其他的字符串、数组、切片遍历方式不太一样,虽然也是被还原成三段式循环,但是调用了runtime.mapiterinitruntime.mapiternext函数,因此我需要连带解析这两个函数的代码,接下来首选看一下,遍历Map的时候,range到底被还原成了神马东西?

优化后代码引自:https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-for-range/

变量名含义:

hit => map的迭代器对象

th => 迭代器的元素类型

t => 元素类型

key => 遍历的map的key

val => 遍历的map的value

ha := a
hit := hiter(n.Type)
th := hit.Type
mapiterinit(typename(t), ha, &hit)
for ; hit.key != nil; mapiternext(&hit) {
    // for range m 时 key,val 都没有
    // 讲道理哦,我是不知道忽略k,v遍历map有啥用
    key := *hit.key // for k := range m 时只有他
    val := *hit.val // for k,v := range m 时 key val 都有
}

代码如上述展示,讲道理也没啥可说的,依旧是做一个拷贝,通过hiter函数构造一个新的迭代器对象hit,然后初始化一个用于迭代(遍历)Map的迭代器对象,然后进行初始化,然后使用这个迭代器来进行针对Map的遍历操作,接下来直接看重点,看runtime.mapiterinitruntime.mapiternext这两个鬼东西到底怎么实现的。

我将剔除一些无用代码,如race相关代码

首先看runtime.mapiterinit:

// 传入参数是,map的类型,对应的map,以及迭代器的变量
func mapiterinit(t *maptype, h *hmap, it *hiter) {
	
	// 空map你遍历个p啊
	if h == nil || h.count == 0 {
		return
	}
	
  // 10-21行
  // 就是把要遍历的map里面的一些信息
  // 赋值给迭代器
	it.t = t
	it.h = h
	
 	it.B = h.B
	it.buckets = h.buckets
	if t.bucket.ptrdata == 0 {
		h.createOverflow()
		it.overflow = h.extra.overflow
		it.oldoverflow = h.extra.oldoverflow
	}
	// 这里要注意了
  // fastrand() 会随机分配一个哈希值
  // 迭代会从这个hash值开始
  // 这也是为什么遍历map的时候是随机结果的原因
 	r := uintptr(fastrand())
  // 如果当前桶大于 31-bucketCntBits 
  // 那么就让随机出来的hash值
  // 重新hash 然后做个修改
	if h.B > 31-bucketCntBits {
		r += uintptr(fastrand()) << 31
	}
  // 根据随机的hash 
  // 算出第一个遍历的桶
	it.startBucket = r & bucketMask(h.B)
	it.offset = uint8(r >> h.B & (bucketCnt - 1))
	// 当前的迭代桶
 	it.bucket = it.startBucket

  // 迭代器可以并发初始化
  // 但是 只有一个迭代器
	if old := h.flags; old&(iterator|oldIterator) != iterator|oldIterator {
		atomic.Or8(&h.flags, iterator|oldIterator)
	}
 
	mapiternext(it)
}
 

runtime.mapiterinit函数大概就是如上述所说,但是有一点要注意的是,Map遍历的顺序是随机的,并不是按照固定顺序进行遍历,这是因为官方不想让使用者依赖迭代顺序,具体是为什么?官方回复如下:

Iterating in maps

In Go 1, the order in which elements are visited when iterating over a map using a for range statement is defined to be unpredictable, even if the same loop is run multiple times with the same map. Code should not assume that the elements are visited in any particular order.

This change means that code that depends on iteration order is very likely to break early and be fixed long before it becomes a problem. Just as important, it allows the map implementation to ensure better map balancing even when programs are using range loops to select an element from a map.

 m := map[string]int{"Sunday": 0, "Monday": 1}
 for name, value := range m {
     // This loop should not assume Sunday will be visited first.
     f(name, value)
 }

Updating: This is one change where tools cannot help. Most existing code will be unaffected, but some programs may break or misbehave; we recommend manual checking of all range statements over maps to verify they do not depend on iteration order. There were a few such examples in the standard repository; they have been fixed. Note that it was already incorrect to depend on the iteration order, which was unspecified. This change codifies the unpredictability.

引自:https://golang.org/doc/go1#iteration

接下来继续讲解runtime.mapiternext函数,这个函数hin长,但是我尽量讲完全。

// 传入参数是哪个迭代器
func mapiternext(it *hiter) {
  // 拿出迭代器中保存的那个map
	h := it.h

  // 判断是否处于并发读写状态
  // 如果处于
  // boom!
	if h.flags&hashWriting != 0 {
		throw("concurrent map iteration and map write")
	}
  // map的类型
	t := it.t
	// 迭代器初始化的时候选择的哪个桶
	bucket := it.bucket
  // 当前遍历到的桶
	b := it.bptr
  // 应该是遍历到哪个tophash了的索引
	i := it.i
  // 没搞懂
	checkBucket := it.checkBucket

next:
  // 如果当前遍历的桶是个nil
  // 就找看看有没有能遍历的桶
	if b == nil {
    // 这代表循环结束绕回了最开始迭代的哪个桶
    // 结束了 返回
		if bucket == it.startBucket && it.wrapped {
			// end of iteration
			it.key = nil
			it.elem = nil
			return
		}
    // 如果当前map正在处于搬迁状态
    // 并且 还没搬迁完
		if h.growing() && it.B == h.B {
			 // 那么就从旧桶遍历
			oldbucket := bucket & it.h.oldbucketmask()
      // 算出来要遍历的旧桶的偏移位置
			b = (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
      // 看看这桶需不需要遍历
      // 就是看看桶里有没有玩意儿
			if !evacuated(b) {
				checkBucket = bucket
			} else {
				b = (*bmap)(add(it.buckets, bucket*uintptr(t.bucketsize)))
				checkBucket = noCheck
			}
		} else {
      // 如果不处于增长状态 那就遍历正常的桶
			b = (*bmap)(add(it.buckets, bucket*uintptr(t.bucketsize)))
			checkBucket = noCheck
		}
    // 看看是否循环一圈了
		bucket++
		if bucket == bucketShift(it.B) {
			bucket = 0
			it.wrapped = true
		}
		i = 0
	}
  // 找到了可以遍历的桶了
  // 开始遍历这个桶
	for ; i < bucketCnt; i++ {
    // 通过tophash看看对应位置有没有存储的数据
		offi := (i + it.offset) & (bucketCnt - 1)
		if isEmpty(b.tophash[offi]) || b.tophash[offi] == evacuatedEmpty {
			continue
		}
    // 位偏移算出key
		k := add(unsafe.Pointer(b), dataOffset+uintptr(offi)*uintptr(t.keysize))
		if t.indirectkey() {
			k = *((*unsafe.Pointer)(k))
		}
    // 位偏移算出value的地址
		e := add(unsafe.Pointer(b),
             dataOffset+bucketCnt*uintptr(t.keysize)+uintptr(offi)*uintptr(t.elemsize))
    
    // 下面这个判断与是否处于搬迁状态有关
    // 如果在搬迁期间就用mapaccessK(t, h, k)取出key,value
    // 否则直接操作内存取key和value
		if checkBucket != noCheck && !h.sameSizeGrow() {
			if t.reflexivekey() || t.key.equal(k, k) {
				hash := t.hasher(k, uintptr(h.hash0))
				if hash&bucketMask(it.B) != checkBucket {
					continue
				}
			} else {
				if checkBucket>>(it.B-1) != uintptr(b.tophash[offi]&1) {
					continue
				}
			}
		}
		if (b.tophash[offi] != evacuatedX && b.tophash[offi] != evacuatedY) ||
			!(t.reflexivekey() || t.key.equal(k, k)) {
			it.key = k
			if t.indirectelem() {
				e = *((*unsafe.Pointer)(e))
			}
			it.elem = e
		} else {
			rk, re := mapaccessK(t, h, k)
			if rk == nil {
				continue 
			}
			it.key = rk
			it.elem = re
		}
		it.bucket = bucket
		if it.bptr != b {  
			it.bptr = b
		}
		it.i = i + 1
		it.checkBucket = checkBucket
		return
	}
  // 遍历溢出桶,如果没有则b == nil 
  // 进入找桶逻辑
	b = b.overflow(t)
	i = 0
	goto next
}

runtime.mapiternext函数代码讲完了,可能有点糙糙的,但是大概的的意思都差不多,那么总结一下:

  1. 根据当前迭代器获取到对应的Map,然后根据当前桶是否处于搬迁状态来决定是遍历常规的桶还是遍历旧桶。
  2. 如果当前处于搬迁状态,那么在遍历对应桶的内部的时候会采用mapaccessK(t, h, k)去获取对应的key,value的地址。
  3. 如果不处于搬迁状态,则按照普通的位偏移模式去对key和对应的value进行位偏移取址。

遍历Channel

现在就剩下最后一个东西的遍历了,那就是Channel(终于快写完了),Channel的遍历方式其实和Map差不多,但是有一个问题是需要各位注意的,Show me code!

func main() {
	channel := make(chan int, 10)
	for v := range channel {
		fmt.Println(v)
	}
}

(不要在意这个代码会死锁就是个演示而已)

遍历Channel与遍历其他的数据结构不同的是,遍历Channel只允许range来前有一个接收变量(废话,channel还能返回索引嘛?),这是一个需要注意的问题。

遍历Channel时,在编译期间也进行了把range转换为传统三段式循环的代码,接下来看一下转换后代码的各个变量的含义,以及转换后的代码:

优化后代码引自:https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-for-range/

变量名含义:

hb => 可以理解为在通常接收通道信息时 value ,ok := <-channel 中的ok

hv1 => 可以理解为在通常接收通道信息时 value ,ok := <-channel 中的value

v1 => for v := range channel 中的 v

ha := a
hv1, hb := <-ha
for ; hb != false; hv1, hb = <-ha {
    v1 := hv1
    hv1 = nil
    ...
}

上述代码是经过编译器转换成传统三段式循环之后的代码,接下来讲解一下:

  1. 老规矩,先做一份遍历对象的拷贝。
  2. 使用ok-idom 方式进行Channel数据的接收,此处有可能会阻塞,如果Channel被关闭,则hb==false
  3. 然后进入循环,把拿到的值拷贝给v1,然后hv1置空,是为了一个提出的issus,在源码中写了注释:

Zero hv1. This prevents hv1 from being the sole, inaccessible reference to an otherwise GC-able value during the next channel receive. See issue 15281

  1. 然后从2重新开始循环。

坑!有坑!有大坑!

既然range这么Nb,那是否可以经常用呢?我的回答是可以,但是range有一些坑(或者说不是坑只是没正确使用),会导致业务逻辑出现混乱的问题,接下来举几个🌰。

相同的玩意儿?

首先,看代码!

func main() {
	slice := []int{1, 2, 3, 4, 5, 6}

	slicePtr := make([]*int, 0, 10)

	for _, v := range slice {
		slicePtr = append(slicePtr, &v)
	}
	for _, v := range slicePtr {
		fmt.Print(*v, ",")
	}
	// out :
	// 6,6,6,6,6,6,
}

上述代码中,我创建了一个int切片slice,然后又创建了一个*int切片slicePtr,通过使用range来循环slice添加到slicePtr,此时,添加完毕后,我们想要的输出应该是 1,2,3,4,5,6, 但是实际输出却是6,6,6,6,6,6,

这是为什么呢?可以回到 遍历数组和切片一节中看到,在range被修改为传统三段式循环时,在进入循环之前就会创建v1 := hv1,v2 := nil变量,并且在每次循环中针对这两个变量只是产生了覆盖而不是重新申请内存,因此,在range前的两个变量在循环过程中的地址都是相同的(可以跑一下下面的代码去试试,看看输出),因此,上面代码的问题在于,每次循环slicePtr取引用的都是相同地址,在循环后,这个地址存的是slice的最后一个值,即6,所以在输出slicePtr的时候,产生的输出均为6。

func main() {
	slice := []int{1, 2, 3, 4, 5, 6}

	for k, v := range slice {
		fmt.Println(&k, &v)
	}
 
}

那么如何避免这个问题呢?使用一个中间变量即可,正确代码如下:

func main() {
   slice := []int{1, 2, 3, 4, 5, 6}

   slicePtr := make([]*int, 0, 10)

   for _, v := range slice {
      temp := v
      slicePtr = append(slicePtr, &temp)
   }
   for _, v := range slicePtr {
      fmt.Print(*v, ",")
   }
   // out :
   // 1,2,3,4,5,6,
}
到底能循环多久?

首先看一下代码:

func main() {
   slice := []int{1, 2, 3, 4, 5, 6}
   for _, v := range slice {
      slice = append(slice, v)
   }
   fmt.Println(slice)
  // out:
  // [1 2 3 4 5 6 1 2 3 4 5 6]
}

上面的代码,在循环中每次循环进行append,按照常理来说这个循环会一直持续下去,但是为什么只添加了一遍就不在循环了呢?还可以回到 遍历数组和切片一节中看到,在range被修改为传统三段式循环时,在进入循环之前会对原数据产生一次拷贝,并且使用这个拷贝判断循环的最大长度hn,因此,只添加了一次循环的长度就结束了循环。

性能?

既然range这么好用,那么是否有一些其他问题呢?比如说性能问题?,接下来作者就进行三种测试来分别看一下传统的三段式循环和range的性能差异

测试环境采用:

Go 1.16版本

Goland 2021.1 EAP版本

goos: darwin
goarch: amd64
cpu: Intel® Core™ i5-1038NG7 CPU @ 2.00GHz

memory:16GB

遍历切片/数组的性能
var TestArray [100000]int
var TestSlice = make([]int, 100000, 100000)

func BenchmarkArrayRange(b *testing.B) {
	for i := 0; i < b.N; i++ {
		for k, v := range TestArray {
			// io.Discard 抛弃输出
			_, _ = fmt.Fprintln(io.Discard, k, v)
		}
	}
}
func BenchmarkArrayFor(b *testing.B) {
	for i := 0; i < b.N; i++ {
		for j := 0; j < len(TestArray); j++ {
			// io.Discard 抛弃输出
			_, _ = fmt.Fprintln(io.Discard, i, TestArray[i])
		}
	}
}

func BenchmarkSliceRange(b *testing.B) {
	for i := 0; i < b.N; i++ {
		for k, v := range TestSlice {
			// io.Discard 抛弃输出
			_, _ = fmt.Fprintln(io.Discard, k, v)
		}
	}
}
func BenchmarkSliceFor(b *testing.B) {
	for i := 0; i < b.N; i++ {
		for j := 0; j < len(TestSlice); j++ {
			// io.Discard 抛弃输出
			_, _ = fmt.Fprintln(io.Discard, i, TestSlice[i])
		}
	}
}

遍历方式采用gotest的基准测试方式,测试在数组/切片长度为100000的情况下,传统的三段式循环和range的性能差异(结果出来的时候我很惊讶为啥会这么慢)

BenchmarkArrayRange
BenchmarkArrayRange-8 132 8902794 ns/op
BenchmarkArrayFor
BenchmarkArrayFor-8 183 6219940 ns/op

BenchmarkSliceRange
BenchmarkSliceRange-8 136 8848273 ns/op
BenchmarkSliceFor
BenchmarkSliceFor-8 182 6483446 ns/op

从上述测试结果可以看出 采用range遍历数组/切片的时候相比采用普通三段式方式遍历,性能大概差了30%左右,这种性能差异在数据很大的遍历中影响还是很大的,个人理解还是因为在range时会拷贝原数据,对于原数据的拷贝是一个很耗性能的操作,因此会出现30%的性能损耗。

遍历Map

使用Map的时候,想遍历,怕是只有使用range了,也没啥别的办法,那么就只测试一下Map在range下的性能表现:

var TestMap = make(map[int]int, 100000)

func init() {
   for i := 0; i < 100000; i++ {
   reRand:
      key, value := rand.Int(), rand.Int()
      if _, ok := TestMap[key]; ok {
         goto reRand
      } else {
         TestMap[key] = value
      }
   }
}

func BenchmarkRange(b *testing.B) {
   for i := 0; i < b.N; i++ {
      for k, v := range TestMap {
         _, _ = fmt.Fprintln(io.Discard, k, v)
      }
   }
}

同样是100000级别的数据量,测试启动时随机分配,接下来看一下性能表现:

BenchmarkRange
BenchmarkRange-8 62 17623282 ns/op

照比遍历切片要慢了接近70%的性能,不过这也没办法,毕竟除了拿range也没啥别的办法去遍历Map。

总结

关于range的一些用法,坑,和性能也差不多讲完了,大概就这些,可能我也没想到什么其他的了。总之希望可以给各位帮助。

我已开通自己的公众号【Echo的技术笔记】
日后的文章发布会主要在公众号上发布
希望各位关注一下
谢谢大家啦
在这里插入图片描述

  • 13
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值