golang for循环使用细节——瞬间提高你的代码执行速度

本文将带你深入了解 golang 中 关于 for 循环的使用细节,以及如何避免在开发过程中犯一些错误,导致代码执行速度极慢,甚至引发一些未知的错误。别人在看到你的代码时,也不会内心在那吐槽了。。。

案例

在日常开发中经常看到的一些代码样例:

样例一:
for i := 0; i < getCount(); i++ {
    // do something
}

for i := 0; i < len(list); i++ {
    // do something
}
样例二:
for i, v := range list {
    // do something
}

for _, v := range list {
    // do something
}

上面这些示例代码,乍一看去没什么问题,挺工整的是吧,其实有很多坑在里面,下面一一进行分析。

分析

样例一:
  • 首先是样例一的代码,这里采用的循环方式是fori循环,就是通过不断累加 i 值,直到不满足条件后退出,是比较常用的循环方式之一,用来遍历数组/切片,或者执行指定次数的操作。
  • 每次循环累加操作没有问题,循环体内的执行代码暂且不考虑,那么问题在哪呢?就在这个判断循环退出条件上面。
  • 我们看到样例一的代码分别有两个 for 循环,每个 for 循环的退出条件都是一个方法,第一个循环体是我们程序内自定义的方法,获取一个计数值,第二个循环体是 golang 自带的方法,用来获取数组/切片/map/channel等的长度。
  • 我们都知道 for 循环每次执行除了将 i 加1之外,还要判断 i 是否小于指定的条件,而上面的代码中,条件的值是通过方法的返回值获取的,无论是我们自定义的方法,还是系统自带的len()方法。这时,相当于每循环1次都会获取1次方法的返回值,也即执行一次方法体。
  • 这里除了性能问题外,还隐藏着一个风险,就是如果获取条件返回值的方法是加载的配置信息,如配置文件或redis、数据库等,当配置文件变更,或者数据库数据修改,每次循环读取到的值可能会发生变化,这样循环的次数也不再受控制了。

下面我写了个简单的测试代码,一眼就可以看出来问题:

func getCount() int {
	var count int
	for i := 0; i < 10; i++ {
		count++
	}
	fmt.Println("getCount:", count)
	return count
}

func main() {
	for i := 0; i < getCount(); i++ {
		fmt.Println("count:", i)
	}
}

输出如下:

getCount: 10
count: 0
getCount: 10
count: 1
getCount: 10
count: 2
getCount: 10
count: 3
getCount: 10
count: 4
getCount: 10
count: 5
getCount: 10
count: 6
getCount: 10
count: 7
getCount: 10
count: 8
getCount: 10
count: 9
getCount: 10

每次循环除了打印当前的 count 值之外,getCount() 方法中的输出也执行了,说明每次循环都重新调用 getCount() 方法获取 count 值。假设在实际开发中,这个getCount()是一个比较耗时的操作,那么你的代码运行速度将会非常慢。

样例二:
  • 样例二的循环有点不同,是采用的 range 关键字进行循环,类似于 java 中的 foreach,这里的循环是对数组/切片/map/channel等进行迭代,直到数组/切片等最后一个元素,或者 channel 关闭后退出。
  • 知道这里循环退出条件后,就应该知道:循环退出条件不再是影响性能的问题所在。再看一下代码可以发现,样例二的循环每次循环的值保存在v这个变量里面,这个值就是每次循环获取到的元素值,i 是对应的下标,样例二第二个循环是把 i 忽略掉了,因为很多时候用不上下标,就直接采用这种方式忽略返回值,这也是 golang 语言的特性。
  • 知道v是用来存储循环的元素值以后,接下来就会想到,每次循环v的值都会被修改为最新的值,那么,这个修改到底是采取的覆盖还是复制呢?答案是复制,每次循环都会复制新的值到v这个变量里面,这就导致,如果循环的数组/切片/map/channel等的元素是是一个非常大的非指针结构体,那么每次循环都会复制一份内存,所以性能会急剧下降。

下面用一个例子来证明 range 循环是拷贝的值:

func main() {
	persons := []struct {
		no int
	}{
		{no: 1},
		{no: 2},
		{no: 3},
	}
	for _, s := range persons {
		s.no += 10
	}
	fmt.Println(persons)
	personsLen := len(persons)
	for i := 0; i < personsLen; i++ {
		persons[i].no += 100
	}
	fmt.Println(persons)
}

输出结果如下:

[{1} {2} {3}]
[{101} {102} {103}]

persons 是一个长度为 3 的切片,每个元素是一个结构体。
使用 range 迭代时,试图将每个结构体的 no 字段增加 10,但修改无效,因为 range 返回的是拷贝。
使用 fori 迭代时,将每个结构体的 no 字段增加 100,修改有效。

优化

通过上面的分析,知道每个 for 循环的问题后,优化方案就很简单了。

样例一:
count := getCount()
for i := 0; i < count; i++ {
    // do something
}

listLen := len(list)
for i := 0; i < listLen; i++ {
    // do something
}

样例一的优化很简单,就是将循环退出条件的方法提出,用一个临时变量存储,这样每次循环直接读取临时变量即可。

样例二:
for i := range list {
    v := list[i]
    // do something
}

样例二的优化就是不再直接通过 range 获取元素值,而是只获取下标,然后通过下标取值,实际开发中应尽量避免样例二的第二种循环写法,还有一种优化方式就是,将list 中存储的元素设置为结构体指针,这样复制的就只是结构体的地址,性能也不会下降太多。如果list中存储的元素是比较简单的结构,如整形数组/切片这种,也可以使用上面的遍历方式,性能也不会差太多。不过为了避免出现问题,还是不要怕麻烦,尽量采用只返回下标的写法。

以上就是我整理的关于 golang for 循环使用及优化需要注意的地方,有表述不对,或理解错误的欢迎指正。本人也是闲来无聊,分享下开发过程中遇到的一些问题,记录一下而已。

你可能感兴趣的:(Golang,golang)