for 和 range 区别
range 可以用来遍历 “引用三兄弟”:slice、map、chan,还有数组 array。
对于数组和 slice,还可以用 for 来遍历。
基本使用
slice/array
Tip
- 对于 range 循环,循环次数会在循环开始前计算好,在循环过程中修改切片长度并不会改变本次循环的次数。
- 对于空切片,循环次数为 0。
| s := []int{11, 99, 16, 55}
for i, v := range s {
fmt.Println(i, v)
}
// Output:
0 11
1 99
2 16
3 55
|
这种方式中,v 是 s 中元素的一份拷贝,对 v 的修改并不会影响 s 中原本的元素。
| s := []int{11, 99, 16, 55}
for i, v := range s {
v += 10 // v 只是一份拷贝,并不会影响 s 里的元素
}
fmt.Println(s)
// Output:
[11 99 16 55]
|
除非 s 中的元素是指针,则对 v 的修改会影响 s,下面将会讲到。
此外采用下面两种方式也可以修改 s 中的元素。
| s := []int{11, 99, 16, 55}
for i := 0; i < len(s); i++ {
s[i] += 10
}
fmt.Println(s)
// Output:
[21 109 26 65]
|
| s := []int{11, 99, 16, 55}
for i := range s {
s[i] += 10
}
fmt.Println(s)
// Output:
[21 109 26 65]
|
map
Tip
- 迭代过程中,删除还没遍历到的键值对,则该键值对不会被遍历。
- 迭代过程中,如果创建新的键值对,则新的键值对有可能被遍历,也有可能不会被遍历。
- 对于空字典,遍历次数为 0。
| m := map[byte]int{
11: 10,
22: 20,
33: 30,
44: 40,
}
for k, v := range m {
v += 5 // 仅仅是对拷贝的修改,不会影响 m
fmt.Println(k, v)
}
fmt.Println(m)
// Output:
22 25
33 35
44 45
11 15
map[11:10 22:20 33:30 44:40]
|
同样这种方式对 v 进行修改是不会影响 m 的。
下面这种就可以
| m := map[byte]int{
11: 10,
22: 20,
33: 30,
44: 40,
}
for k := range m {
m[k] += 1 // 这种方式就会修改到 m 的元素
fmt.Println(k, m[k])
}
fmt.Println(m)
// Output:
11 11
22 21
33 31
44 41
map[11:11 22:21 33:31 44:41]
|
chan
Tip
- 发送给信道的值,可以通过 range 遍历,直到信道被关闭,才会退出循环。
- 如果是未分配内存的 nil 信道,循环将永远阻塞。
| ch := make(chan byte)
go func() {
ch <- 'b'
ch <- 'o'
ch <- 'i'
ch <- 'i'
close(ch)
}()
for c := range ch {
fmt.Println(string(c))
}
// Output:
b
o
i
i
|
性能比较
接下来我们主要针对基本类型和结构体类型,比较他们使用 for 和 range 的性能。
只有 slice 和 array 才有办法 for 和 range 都使用,所以接下来的比对如下:
|
for |
range |
基本类型 |
for + []int |
range + []int |
结构体 |
for + []struct |
range + []struct |
for + []int
VS range + []int
主要方式有 3 种:
for i:=0; i < len(s); i++
for i := range s
for i, v := range s
Summary
先说结论:遍历 []int
,三种方式并没太大差别。
[]int
| // 生成切片
func geneI(n int) []int {
nums := make([]int, 0, n)
rand.Seed(time.Now().Unix())
for z := 0; z < n; z++ {
nums = append(nums, rand.Int())
}
return nums
}
// for 遍历
func I_For(nums []int) {
n := len(nums)
for i := 0; i < n; i++ {
_ = nums[i]
}
}
// range i 遍历
func I_RangeI(nums []int) {
for i := range nums {
_ = nums[i]
}
}
// range iv 遍历
func I_RangeIV(nums []int) {
for i, v := range nums {
_, _ = i, v
}
}
func BenchmarkI_For(b *testing.B) {
nums := geneI(1 << 20)
for i := 0; i < b.N; i++ {
I_For(nums)
}
}
func BenchmarkI_RangeI(b *testing.B) {
nums := geneI(1 << 20)
for i := 0; i < b.N; i++ {
I_RangeI(nums)
}
}
func BenchmarkI_RangeIV(b *testing.B) {
nums := geneI(1 << 20)
for i := 0; i < b.N; i++ {
I_RangeIV(nums)
}
}
|
执行后的结果如下:
| $ go version
go1.16.6 linux/amd64
$ go test . -bench=I_ -benchmem
goos: linux
goarch: amd64
cpu: Intel(R) Core(TM) i5-7300HQ CPU @ 2.50GHz
BenchmarkI_For-4 2685 431122 ns/op 3124 B/op 0 allocs/op
BenchmarkI_RangeI-4 2686 431759 ns/op 3123 B/op 0 allocs/op
BenchmarkI_RangeIV-4 2688 432650 ns/op 3120 B/op 0 allocs/op
PASS
ok 3.586s
|
从结果可以看出,在遍历基本类型切片时,不管是时间上(ns/op),还是空间上(B/op),还是系统调用上(allocs/op),都没啥差别。
现在是 2021-8-19,以上是 go 1.16.6 版本测试的结果,并没有太大差别,不像网上说的 for 比 range 快。
[]struct
| const N = 1 << 20
type Per struct {
ins [2048]byte
age int
}
func geneS(n int) []Per {
persons := make([]Per, 0, n)
rand.Seed(time.Now().Unix())
for i := 0; i < n; i++ {
persons = append(persons, Per{age: rand.Int(), ins: [2048]byte{'a'}})
}
return persons
}
func S_For(persons []Per) {
n := len(persons)
for i := 0; i < n; i++ {
_ = persons[i].age
}
}
func S_RangeI(persons []Per) {
for i := range persons {
_ = persons[i].age
}
}
func S_RangeIV(persons []Per) {
for i, v := range persons {
_, _ = i, v.age
}
}
func BenchmarkS_For(b *testing.B) {
persons := geneS(N)
b.ResetTimer()
for i := 0; i < b.N; i++ {
S_For(persons)
}
}
func BenchmarkS_RangeI(b *testing.B) {
persons := geneS(N)
b.ResetTimer()
for i := 0; i < b.N; i++ {
S_RangeI(persons)
}
}
func BenchmarkS_RangeIV(b *testing.B) {
persons := geneS(N)
b.ResetTimer()
for i := 0; i < b.N; i++ {
S_RangeIV(persons)
}
}
|
运行结果
| $ go test . -bench=S_ -benchmem
goos: linux
goarch: amd64
cpu: Intel(R) Core(TM) i5-7300HQ CPU @ 2.50GHz
BenchmarkS_For-4 2850 421009 ns/op 0 B/op 0 allocs/op
BenchmarkS_RangeI-4 2850 420993 ns/op 0 B/op 0 allocs/op
BenchmarkS_RangeIV-4 2848 421131 ns/op 0 B/op 0 allocs/op
PASS
ok 7.330s
|
可以看到基本也没太大区别。
[]*struct
Summary
结论:即使是结构体指针切片,跑出来的结果几乎相差无几
| func PS_For(persons []*Per) {
n := len(persons)
for i := 0; i < n; i++ {
_ = persons[i].age
}
}
func PS_RangeI(persons []*Per) {
for i := range persons {
_ = persons[i].age
}
}
func PS_RangeIV(persons []*Per) {
for i, v := range persons {
_, _ = i, v.age
}
}
func BenchmarkPS_For(b *testing.B) {
persons := genePS(N)
b.ResetTimer()
for i := 0; i < b.N; i++ {
PS_For(persons)
}
}
func BenchmarkPS_RangeI(b *testing.B) {
persons := genePS(N)
b.ResetTimer()
for i := 0; i < b.N; i++ {
PS_RangeI(persons)
}
}
func BenchmarkPS_RangeIV(b *testing.B) {
persons := genePS(N)
b.ResetTimer()
for i := 0; i < b.N; i++ {
PS_RangeIV(persons)
}
}
|
运行结果:
| $ go test . -bench=S_ -benchmem
goos: linux
goarch: amd64
cpu: Intel(R) Core(TM) i5-7300HQ CPU @ 2.50GHz
BenchmarkS_For-4 83856697 14.13 ns/op 0 B/op 0 allocs/op
BenchmarkS_RangeI-4 66149228 17.66 ns/op 0 B/op 0 allocs/op
BenchmarkS_RangeIV-4 72271676 16.61 ns/op 0 B/op 0 allocs/op
BenchmarkPS_For-4 71399964 21.43 ns/op 0 B/op 0 allocs/op
BenchmarkPS_RangeI-4 63293829 18.62 ns/op 0 B/op 0 allocs/op
BenchmarkPS_RangeIV-4 63576286 18.57 ns/op 0 B/op 0 allocs/op
PASS
ok 7.565s
|
能修改元素的遍历
前面说 for i, v := range s
这种方式,v 是一份拷贝,对 v 的修改不会影响 s 中的元素。
但是如果 v 是个指针,对 v 的修改还是会影响的。
| a := 10
b := 20
c := 30
s := []*int{&a, &b, &c}
fmt.Println(a, b, c)
for _, v := range s {
*v += 1
}
fmt.Println(a, b, c)
// Output:
10 20 30
11 21 31
|
再举个结构体的栗子:
| type Person struct {
name string
age int
}
persons := make([]*Person, 0, 3)
persons = append(persons, &Person{age: 15}, &Person{age: 25}, &Person{age: 35})
fmt.Println(persons[0], persons[1], persons[2])
for _, v := range persons {
v.age += 1
}
fmt.Println(persons[0], persons[1], persons[2])
// Output:
&{ 15} &{ 25} &{ 35}
&{ 16} &{ 26} &{ 36}
|
总结
遍历总共有 3 种方式:
- for:
for i:=0; i<n; i++
- rangeI:
for i := range items
- rangeIV:
for i, v := range items
三种方式都各有所长。其中 rangeIV 中 v 是 items 中元素的一份拷贝,如果 v 不是指针,那么对于 v 的修改并不会影响 items 里面的元素,如果 v 是指针,则能够影响。
正是因为 v 是一份拷贝,所以在一些旧版本中可能会因此降低了性能,但在新版本(至少 go 1.16.6 ,我并没有去求证)中,从基准测试来看是没有什么区别,可能在新版本中做了优化。