Go中Range方法你整明白了吗?

先看个例子

请说出下面函数的输出值分别是什么?

func listing1() {
    a := [3]int{0, 1, 2}

    for i, v := range a {
        a[2] = 10
        if i == 2 {
            fmt.Println(v)
        }
    }
}

func listing2() {
    a := [3]int{0, 1, 2}
    for i := range a {
        a[2] = 10
        if i == 2 {
            fmt.Println(a[2])
        }
    }
}

func listing3() {
    a := [3]int{0, 1, 2}
    for i, v := range &a {
        a[2] = 10
        if i == 2 {
            fmt.Println(v)
        }
    }
}

结果分别是 2, 10, 10。 第一个函数 listing1 不容易理解。 我们就来深入了解下 range 方法的用法。

range

Go 语言中 range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对。

可以速记为msc , 这里我们再次列出来之前的速记,我们复习一下。

  1. 可以与nil进行比较的是: mscfip
  2. 可以使用make进行初始化的是: msc
  3. 可以使用range遍历的是: msc

range for slice

注释解释了遍历slice的过程: 下面是源码中的代码

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

遍历 slice 前会先获取 slice 的长度 len_temp 来作为循环次数,循环体中,每次循环会先获取元素值,如果 for-range 中接收 indexvalue 的话,则会对 indexvalue 进行一次赋值。数组与数组指针的遍历过程与 slice 基本一致。

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

可以理解为range 在编译阶段已经对长度和里面的值进行了拷贝复制出来。

循环永动机

如果我们在遍历数组的同时修改数组的元素,能否得到一个永远都不会停止的循环呢?你可以尝试运行下面的代码:

func main() {
    arr := []int{1, 2, 3}
    for _, v := range arr {
            arr = append(arr, v)
    }
    fmt.Println(arr)
}

其输出结果为: 1 2 3 1 2 3

上述代码的输出意味着循环只遍历了原始切片中的三个元素,我们在遍历切片时追加的元素不会增加循环的执行次数,所以循环最终还是停了下来。

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

遍历 map 时没有指定循环次数,循环体与遍历 slice 类似。由于 map 底层实现与 slice 不同,map 底层使用 hash 表实现的。

插入数据位置是随机的,所以遍历过程中新插入的数据不能保证遍历到。

range for channel

// The loop we generate:
// for {
//     index_temp, ok_temp = <-range
//     if !ok_temp {
//       break
//     }
//     index = index_temp
//     original body
// }

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

注意:

  • 上述注释中 index_temp 实际上描述是有误的,应该为 value_temp,因为 index 对于 channel 是没有意义的。
  • 使用 for-range 遍历 channel 时只能获取一个返回值。

Go 1.22 对for循环进行了两个大更新

Go 1.22 版本于 2024 年 2 月 6 日正式向世界宣告了版本的发布 。此此版本也带来了一定for range的更新。

循环不再共享循环变量

Go1.22之前版本for 循环声明的变量只创建一次,并在每次迭代中进行更新,这会导致遍历时访问value时实际上都是访问的同一个地址的值。

func main() {
    done := make(chan bool)

    values := []string{"xiao", "xu", "code"}
    for _, v := range values {
        go func() {
            fmt.Println(v)
            done <- true
        }()
    }

    // 等待所有的 goroutine 执行结束
    for _ = range values {
        <-done
    }
}
// 上述代码运行结果如下所示: code, code, code

这三个创建的 goroutine 都在打印同一个变量 v,所以它们通常会打印出 "code"、"code"、"code",而不是以某种顺序打印出 "xiao"、"xu" 和 "code"。

🚩 这就是共享循环变量造成的问题!

这个比较好理解,这个循环的 v 只创建一次,在每次循环的时候都会更新,而 闭包在访问 v 时实际上都访问的是同一个内存地址,所以最终打印的都是同一个值。

解决办法:

在Go版本不变的情况下,可以通过下面两种方式修改代码避免这个问题。

1:将for循环中传入v,代码改造如下

values := []string{"xiao", "xu", "code"}
for _, v := range values {
    go func(v string) {
        fmt.Println( v)
        done <- true
    }(v)
}

2:在循环中重新定义一个变量进行再次赋值

values := []string{"xiao", "xu", "code"}
for _, v := range values {
    value := v
    go func() {
        fmt.Println( value)
        done <- true
    }()
}

Go1.22版本

不过这个问题在1.22版本已经得到处理了,大家用这个版本的时候可以放心使用了,太爽了吧!

我们在1.22版本上运行和1.21一样的代码

func main() {
    done := make(chan bool)

    values := []string{"xiao", "xu", "code"}
    for _, v := range values {
        go func() {
            fmt.Println(v)
            done <- true
        }()
    }

    // 等待所有的 goroutine 执行结束
    for _ = range values {
        <-done
    }
}
// 上述代码运行结果如下所示:code, xiao, xu

for 循环的每次迭代都会创建新变量,每次循环迭代各自的变量,以避免意外共享错误。上面一模一样的代码,输出结果不再是固定的 code。

支持整数范围进行循环迭代

在 Go 1.22 版本之前, for range 仅支持对 array or slicestringmapchannel 类型的进行迭代。

而自 Go 1.22 版本起,新增了整数类型的迭代支持,我们能够直接使用整数进行循环迭代。

下面同样列举不同版本的例子,看看差异性!

Go1.22 之前版本

func main() {
    for i := range 5 {
        fmt.Println("小许code", i)
    }
}

不支持遍历整数范围,这个range 5就直接提示报错了,编译当然有问题了

.\main.go:15:17: cannot range over 5 (untyped int constant)

Go1.22版本

func main() {
    for i := range 5 {
        fmt.Println("小许code", i)
    }
}

巩固一下

func main() {
    ch1 := make(chan int, 3)
    go func() {
        ch1 <- 0
        ch1 <- 1
        ch1 <- 2
        close(ch1)
    }()

    ch2 := make(chan int, 3)
    go func() {
        ch2 <- 10
        ch2 <- 11
        ch2 <- 12
        close(ch2)
    }()

    ch := ch1
    for v := range ch {
        fmt.Println(v)
        ch = ch2
    }
}

// 输出结果为 0,1,2

已关闭的 channel 仍然有数据

不能依据从通道中获得的第二个判断,通道是否关闭了。

func main() {
    ch := make(chan int, 3)
    ch <- 1
    close(ch)
    // 第一次从通道中获取数据
    value, ok := <-ch
    fmt.Println("第一次从通道中获取数据 value", value) //  1
    fmt.Println("第一次从通道中获取数据 ok", ok)       //  true
    // 第二次从通道中获取数据
    value, ok = <-ch
    fmt.Println("第二次从通道中获取数据 value", value) //  0
    fmt.Println("第二次从通道中获取数据 ok", ok)       //  false
}

总结

  • 遍历过程中可以适情况丢弃 indexvalue,可以一定程度上提升性能
  • 遍历 channel 时,如果 channel 中没有数据,可能会阻塞
  • 使用 index,value 接收 range 返回值会发生一次数据拷贝
  • 已经for-range 在 go1.22中的版本升级
关于我
loading