Go-For Range 性能研究

本文深入探讨了Go语言中For-Range循环在遍历Slice切片和Map集合时的性能表现,对比了传统For循环,并通过基准测试揭示了性能差异的原因。

      文章转载地址:https://www.flysnow.org/2018/10/20/golang-for-range-slice-map.html

      如果我们要遍历某个数组,Map 集合、Slice 切片等,Go 语言(Golang) 为我们提供了比较好的 For Range 方式。

range 是一个关键字, 表示范围,和 for 配合使用可以迭代 数组、Map、Slice等集合,用法比较简洁,那么,这种

迭代方式和 for i=0;i<N;i++ 对比,性能怎么样呢?下面通过 Go 的基准测试对比一下两者的性能

      For-Range 的基本使用

      for range 的使用非常简单,这里演示两种集合类型的使用

package main

import "fmt"

func main() {
	ages := []string{"10","20","30"}

	for i,age := range ages {
		fmt.Println(i,age)
	}
}

  这里是针对 Slice 切片的迭代使用,使用 range 关键字返回两个变量 i,age ,第一个是 Slice 切片的索引,第二个

是 Slice 切片的内容,打印结果如下:

0 10
1 20
2 30

  下面再看看 Map 的 for range 使用示例:

package main

import "fmt"

func main() {
	ages:=map[string]int{"张三":15,"李四":20,"王武":36}

	for name,age:=range ages{
		fmt.Println(name,age)
	}
}

  在使用for range迭代map的时候,返回的第一个变量是key,第二个变量是value,也就是我们例子中对应的name和ages

。我们运行程序看看输出结果:

张三 15
李四 20
王五 36

  常规 For 循环对比

       比如对于 Slice 切片,我们有两种迭代方式:一种是常规的for i:=0;i<N;i++的方式;一种是for range的方式,如下示例:

package main_test

import "testing"

const N  = 1000

// 常规 for 迭代 slice
func ForSlice(s []string) {
	len := len(s)
	for i := 0; i < len; i++ {
		_, _ = i,s[i]
	}
}

// for range 迭代 slice
func RangeForSlice(s []string) {
	for i, v := range s {
		_, _ = i, v
	}
}

// 初始化 slice
func initSlice() []string{
	s := make([]string,N)

	for i := 0;i < N;i++ {
		s[i] = "www.flysnow.org"
	}
	return s
}

// 基准测试函数
func BenchmarkForSlice(b *testing.B) {
	s := initSlice()

	b.ResetTimer()
	for i := 0;i < b.N;i++ {
		ForSlice(s)
	}
}

func BenchmarkRangeForSlice(b *testing.B) {
	s := initSlice()

	b.ResetTimer()
	for i := 0;i < b.N;i++  {
		RangeForSlice(s)
	}
}

  输出结果如下:

goos: windows
goarch: amd64
BenchmarkForSlice-8              5000000               303 ns/op
BenchmarkRangeForSlice-8         3000000               512 ns/op
PASS
ok      _/E_/GoProject/development/src  4.692s

  从上面的输出结果可以看到,常规的 For 循环的性能更高。主要是因为 for range 是每次对循环元素的拷贝,而

for 循环,它获取集合内元素是通过 s[i],这种索引指针引用的方式,要比拷贝性能高得多

      那么既然是元素拷贝的问题,我们在使用 range 方式迭代 slice 时候的目的也是为了获取元素,现在换一种方式实现 for range:

// for range 迭代 slice
func RangeForSlice(s []string) {
	for i, _ := range s {
		_, _ = i, s[i]
	}
}

  输出结果:

goos: windows
goarch: amd64
BenchmarkForSlice-8              5000000               303 ns/op
BenchmarkRangeForSlice-8         5000000               308 ns/op
PASS
ok      _/E_/GoProject/development/src  4.218s

  结果和常规的 for 循环一样。原因是我们通过 _ 舍弃了元素的复制,然后通过 s[i] 方式获取迭代的元素

       Map 遍历

       对于 map 来说,我们并不能使用 for i=0;i<N;i++ 的方式,大部分我们使用 for range 的方式:

package main_test

import (
	"fmt"
	"testing"
)

const N  = 1000

// for range For map
func RangeForMap1(m map[int]string) {
	for k, v := range m{
		_,_ = k,v
	}
}

// 初始化 map
func initMap() map[int]string  {
	m := make(map[int]string,N)

	for i := 0;i < N;i++ {
		m[i] = fmt.Sprint("www.flysnow.org",i)
	}

	return m
}

func BenchmarkRangeForMap1(b *testing.B) {
	m := initMap()

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		RangeForMap1(m)
	}
}

  运行结果如下:

goos: windows
goarch: amd64
BenchmarkRangeForMap1-8           100000             14535 ns/op
PASS
ok      _/E_/GoProject/development/src  2.333s

  相比较 slice,Map 遍历的性能更差。现在,我们使用上面优化遍历 slice 的方式优化遍历 map,减少值拷贝,如下示例:

func RangeForMap2(m map[int]string) {
	for k, _ := range m{
		_,_ = k,m[k]
	}
}

func BenchmarkRangeForMap2(b *testing.B) {
	m := initMap()

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		RangeForMap2(m)
	}
}

  运行结果如下:

goos: windows
goarch: amd64
BenchmarkRangeForMap1-8           100000             14290 ns/op
BenchmarkRangeForMap2-8           100000             22240 ns/op
PASS
ok      _/E_/GoProject/development/src  4.929s

  我们看到,优化后的结果性能明显下降了,这和我们上面测试 slice 不一样,这次没有提升反而下降了

For Range 原理

        range for slice:

  // The loop we generate:
  //   len_temp := len(range)
  //   range_temp := range
  //   for index_temp = 0; index_temp < len_temp; index_temp++ {
  //           value_temp = range_temp[index_temp]
  //           index = index_temp
  //           value = value_temp
  //           original body
  //   }

  遍历 slice 前先是对要遍历的 slice 做一个拷贝,然后获取 slice 的长度作为循环次数,循环体中每次循环

会先获取元素值,我们还可以看到遍历过程中每次迭代都会对 index 和 value 进行赋值,如果数据量比较大或

者 value 为 string 时,对 value 的赋值操作可能是多余的,所以在上面我们使用 range 遍历 slice 的时候,可以

忽略 value,使用 slice[index] 的方式提升性能

       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
  //   }

  看上面的实现方式,结合我们使用 for range slice 的 _ 优化方式,我们可以看到看似减少了一次赋值操作,但

是通过 key 查找 value 的性能消耗高于赋值消耗,这就是为什么优化没有起到作用 

转载于:https://www.cnblogs.com/leeyongbard/p/10394820.html

<think>好的,我现在要回答关于Go语言中for range和普通for循环(for i)的区别的问题。首先,我需要回忆一下自己对这两个循环的理解,然后结合提供的引用资料来整理出清晰的回答。 首先,for循环在Go语言中是最基本的循环结构,通常用于遍历数组、切片等数据结构。而for range则是更高级的语法糖,可以更方便地遍历各种数据结构,比如数组、切片、字符串、map和channel。根据引用[3],for range遍历不同的数据结构会返回不同的值,比如数组、切片、字符串返回索引和值,map返回键和值,channel则只返回值。 接下来,我需要比较两者的性能差异。根据引用[2],对于切片来说,普通的for i循环可能比for range慢一些,但对于数组则没有区别。这说明底层的数据结构可能影响了循环的性能。比如,数组在内存中是连续存储的,而切片是对数组的引用,可能有额外的间接访问导致性能差异。不过,这个结论需要进一步验证,比如查看编译后的代码或者基准测试结果。 然后,关于变量作用域的问题。在for range循环中,每次迭代的变量是复用的,这可能导致闭包中的引用问题。例如,在并发中使用循环变量时,如果不正确复制变量,可能会导致所有goroutine都引用同一个变量。而普通的for i循环中,每次迭代的变量是独立的,不会出现这种问题。引用[1]可能提到这一点,但需要具体例子说明。 另外,遍历过程中的值复制也是一个关键点。for range在遍历时会复制元素的值到临时变量,而for i则是直接通过索引访问元素。如果元素是结构体等较大对象,这种复制可能会带来性能开销。例如,在引用[4]的代码中,遍历MultiplePaperQuestions时,每个元素的修改可能不会影响到原切片,因为for range创建的是副本。这需要用户注意是否需要修改原数据,从而决定是否使用索引访问。 还需要讨论遍历的可变性问题。使用for range遍历切片或数组时,如果在循环体内修改了原数据结构的长度(比如追加或删除元素),可能会影响遍历的行为。而普通的for i循环可能更容易控制索引的变化,但同样需要谨慎操作,避免越界。 然后,关于不同类型的数据结构遍历,比如map和channel。for range是唯一遍历map的方式,而普通for循环无法做到。遍历channel时,for range会一直阻塞直到channel关闭,这也是普通for循环无法直接实现的。 总结下来,两者的区别主要在以下几个方面:适用的数据结构、性能差异、变量作用域、值复制行为、遍历中的修改安全性以及代码简洁性。需要将这些点分门别类地整理,并给出示例代码来说明问题,比如引用[4]中的代码可能存在什么问题,如何修正。 最后,生成的相关问题需要覆盖性能分析、优化建议、适用场景以及常见错误,这样用户可以根据回答进一步探索。</think>### Go语言中for range与普通for循环的区别 在Go语言中,`for range`和普通`for i`循环都能用于遍历数据结构,但两者在实现细节、适用场景和性能表现上有显著差异。 --- #### 1. **语法与适用数据结构** - **`for range`** 支持遍历数组、切片、字符串、map和channel[^3]: ```go // 遍历切片 for index, value := range slice { ... } // 遍历map for key, val := range m { ... } // 遍历channel(直到关闭) for item := range ch { ... } ``` 每次迭代返回索引(或键)和值,但**值是对原元素的副本**,修改副本不会影响原数据。 - **普通`for i`** 仅适用于数组、切片等可通过索引访问的结构: ```go for i := 0; i < len(slice); i++ { value := slice[i] // 直接访问元素 } ``` 直接通过索引操作原数据,修改会影响原数据结构。 --- #### 2. **性能差异** - **数组**:`for i`和`for range`性能相同,因为数组是值类型,`for range`会复制整个数组到临时变量[^2]。 - **切片**:`for i`通常比`for range`快,因为`for range`需要处理底层数组的引用和值复制。 - **map和channel**:只能使用`for range`,普通循环无法遍历。 --- #### 3. **变量作用域与闭包陷阱** - **`for range`的变量复用** 每次迭代的变量(如`index`和`value`)是复用的,可能导致闭包引用错误: ```go var funcs []func() slice := []int{1, 2, 3} for _, v := range slice { funcs = append(funcs, func() { fmt.Println(v) }) } // 输出全为3(v是复用的临时变量) ``` 修正方法:在循环内创建局部变量: ```go for _, v := range slice { vCopy := v funcs = append(funcs, func() { fmt.Println(vCopy) }) } ``` - **`for i`的独立作用域** 每次迭代的`i`是独立的,闭包中直接引用不会出错: ```go for i := 0; i < len(slice); i++ { funcs = append(funcs, func() { fmt.Println(slice[i]) }) } ``` --- #### 4. **遍历中的修改行为** - **`for range`的值副本** 修改迭代变量不会影响原数据: ```go slice := []int{1, 2, 3} for _, v := range slice { v *= 2 // 原slice仍为[1,2,3] } ``` 需通过索引修改原数据: ```go for i := range slice { slice[i] *= 2 // 原slice变为[2,4,6] } ``` - **`for i`直接操作原数据** 通过索引可直接修改元素: ```go for i := 0; i < len(slice); i++ { slice[i] *= 2 } ``` --- #### 5. **代码示例分析** 引用[4]中的代码: ```go MultiplePaperQuestions := make([]MultiplePaperQuestion, len(p.MultipleQuestionIds)) for i, multiplePaperQuestion := range MultiplePaperQuestions { multiplePaperQuestion.MultipleQuestion = MultipleQuestions[i] multiplePaperQuestion.Score = p.MultipleQuestionEachScores[i] } ``` 此代码**无法修改原切片元素**,因为`multiplePaperQuestion`是副本。应改为通过索引操作: ```go for i := range MultiplePaperQuestions { MultiplePaperQuestions[i].MultipleQuestion = MultipleQuestions[i] MultiplePaperQuestions[i].Score = p.MultipleQuestionEachScores[i] } ``` --- ### 总结 | 特性 | `for range` | `for i` | |--------------------|--------------------------------------|----------------------------------| | 适用数据结构 | 数组、切片、字符串、map、channel | 数组、切片 | | 性能(切片) | 较慢(需处理引用) | 更快(直接索引访问) | | 变量作用域 | 复用变量,可能引发闭包问题 | 独立作用域 | | 修改原数据 | 需通过索引 | 直接通过索引修改 | | 代码简洁性 | 高(自动处理索引和值) | 低(需手动管理索引) | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值