Go 循环之for循环,仅此一种

目录

一、for 循环介绍

日常编码过程中,我们常常需要重复执行同一段代码,这时我们就需要循环结构来帮助我们控制程序的执行顺序。一个循环结构会执行循环体中的代码直到结尾,然后回到开头继续执行。 主流编程语言都提供了对循环结构的支持,绝大多数主流语言,比如:Python 提供了不止一种的循环语句,但 Go 却只有一种,也就是 for 语句

二、for 循环结构

2.1 基本语法结构

Go语言的for循环的一般结构如下:

for 初始语句;条件表达式;结束语句{
循环体语句
}
  1. 初始语句:在循环开始前执行一次的初始化操作,通常用于声明计数器或迭代变量的初始值。
  2. 条件表达式:循环会在每次迭代之前检查条件表达式,只有当条件为真时,循循环才会继续执行。如果条件为假,循环结束。
  3. 结束语句:在每次迭代之后执行的操作,通常用于更新计数器或迭代变量的值。

以下是一个示例,演示了不同类型的for循环基本用法:

var sum int
for i := 0; i < 10; i++ {
sum += i
}
println(sum)

这种 for 语句的使用形式是 Go 语言中 for 循环语句的进形式。我们用一幅流程图来直观解释一下上面这句 for 循环语句的组成部分,以及各个部分的执行顺序:

从图中我们看到,经典 for 循环语句有四个组成部分(分别对应图中的①~④)。我们按顺序拆解一下这张图。

图中①对应的组成部分执行于循环体(③ )之前,并且在整个 for 循环语句中仅会被执行一次,它也被称为循环前置语句。我们通常会在这个部分声明一些循环体(③ )或循环控制条件(② )会用到的自用变量,也称循环变量或迭代变量,比如这里声明的整型变量 i。与 if 语句中的自用变量一样,for 循环变量也采用短变量声明的形式,循环变量的作用域仅限于 for 语句隐式代码块范围内。

图中②对应的组成部分,是用来决定循环是否要继续进行下去的条件判断表达式。和 if 语句的一样,这个用于条件判断的表达式必须为布尔表达式,如果有多个判断条件,我们一样可以由逻辑操作符进行连接。当表达式的求值结果为 true 时,代码将进入循环体(③)继续执行,相反则循环直接结束,循环体(③)与组成部分④都不会被执行。

前面也多次提到了,图中③对应的组成部分是 for 循环语句的循环体。如果相关的判断条件表达式求值结构为 true 时,循环体就会被执行一次,这样的一次执行也被称为一次迭代(Iteration)。在上面例子中,循环体执行的动作是将这次迭代中变量 i 的值累加到变量 sum 中。

图中④对应的组成部分会在每次循环体迭代之后执行,也被称为循环后置语句。这个部分通常用于更新 for 循环语句组成部分①中声明的循环变量,比如在这个例子中,我们在这个组成部分对循环变量 i 进行加 1 操作。

2.2 省略初始值

for 循环的初始语句可以被忽略,但是必须要写初始语句后面的分号

	i := 0
for ; i < 10; i++ {
fmt.Println(i)
}

2.3 省略初始语句和结束语句

for循环的初始语句和结束语句都可以省略,例如:

func main() {
var i int
for i < 10 {
fmt.Println(i)
i++
}
}

这种写法类似于其他编程语言中的while,在while后添加一个条件表达式,满足条件表达式时持续循环,否则结束循环。

2.4 无限循环

无限循环是一种循环结构,它会一直执行,而不受循环条件的限制,同时省略了初始语句,条件表达式,结束语句。基本语法格式如下:

for {
循环体语句
}

它的形式等价于:

for true {
// 循环体代码
}

或者等价于:

for ; ; {
// 循环体代码
}

在日常使用时,建议你用它的最简形式,也就是for {...},更加简单。

举个栗子:

	for {
fmt.Println("这是一个死循环!")
}

无限循环通常在编程中用于执行需要持续运行的任务,如服务器监听、事件处理等。

2.5 for 循环支持声明多循环变量

Go 语言的 for 循环支持声明多循环变量,并且可以应用在循环体以及判断条件中,比如下面就是一个使用多循环变量的、稍复杂的例子:

	var sum int
for i, j, k := 0, 1, 2; (i < 20) && (j < 10) && (k < 30); i, j, k = i+1, j+1, k+5 {
sum += (i + j + k)
println(sum)
}

在这个例子中,我们声明了三个循环自用变量 i、j 和 k,它们共同参与了循环条件判断与循环体的执行。这段代码的执行流程解释如下:

  1. 开始时,i 被初始化为 0,j 被初始化为 1,k 被初始化为 2,sum 被初始化为 0。
  2. 进入循环。在每次迭代中,首先检查三个条件:i < 20j < 10k < 30。只有在这三个条件都为真时,循环才会继续执行。
  3. 在每次迭代中,计算 i + j + k 的和,并将结果添加到 sum 中。
  4. 使用 println 函数打印 sum 的当前值。
  5. 继续迭代,ijk 分别增加 1、1 和 5。
  6. 重复步骤 2、3、4 直到其中一个条件不再满足。在这种情况下,当 i 大于或等于 20、j 大于或等于 10 或 k 大于或等于 30 时,循环结束。

2.6 小练习:打印九九乘法表

	for y := 1; y <= 9; y++ {
// 遍历, 决定这一行有多少列
for x := 1; x <= y; x++ {
fmt.Printf("%d*%d=%d ", x, y, x*y)
}
// 手动生成回车
fmt.Println()
}

输出结果如下:

1*1=1
1*2=2 2*2=4
1*3=3 2*3=6 3*3=9
1*4=4 2*4=8 3*4=12 4*4=16
1*5=5 2*5=10 3*5=15 4*5=20 5*5=25
1*6=6 2*6=12 3*6=18 4*6=24 5*6=30 6*6=36
1*7=7 2*7=14 3*7=21 4*7=28 5*7=35 6*7=42 7*7=49
1*8=8 2*8=16 3*8=24 4*8=32 5*8=40 6*8=48 7*8=56 8*8=64
1*9=9 2*9=18 3*9=27 4*9=36 5*9=45 6*9=54 7*9=63 8*9=72 9*9=81

执行过程如下:

  1. for y := 1; y <= 9; y++:这是外部的for循环,它初始化一个名为 y 的循环变量,从1开始,每次迭代递增1,一直到 y 的值小于或等于9。
  2. 内部的for循环 for x := 1; x <= y; x++:这是内部的for循环,用于控制每行的列数。循环变量 x 从1开始,每次迭代递增1,一直到 x 的值小于或等于 y。这确保了每一行都只打印与行数相等或更小的列数。
  3. fmt.Printf("%d*%d=%d ", x, y, x*y):在内部循环中,这一行代码用于打印每个乘法表达式。它使用 fmt.Printf 函数,打印了一个格式化的字符串,其中 %d 是占位符,分别用 xyx*y 的值替换。这将打印类似 "11=1 "、"12=2 "、"2*2=4 " 的格式。
  4. fmt.Println():在内部循环结束后,使用 fmt.Println 打印一个换行符,以将每行的输出分开。

三、for range(键值循环)

3.1 基本介绍

在编程中,经常需要遍历和操作集合(如数组、切片、映射等)中的元素。Go语言中可以使用for range遍历数组、切片、字符串、map 及通道(channel)。 通过for range遍历的返回值有以下规律:

  1. 数组、切片、字符串返回索引和值。
  2. map返回键和值。
  3. 通道(channel)只返回通道内的值。

3.2 基本语法格式

for range 循环的基本语法格式如下:

for key, value := range collection {
// 循环体代码,使用 key 和 value
}
  • key 是元素的索引或键。
  • value 是元素的值。
  • collection 是要遍历的元素,如字符串、数组、切片、映射等。

举个例子,首先我们使用for 循环基本形式:

var sl = []int{1, 2, 3, 4, 5}
for i := 0; i < len(sl); i++ {
fmt.Printf("sl[%d] = %d\n", i, sl[i])
}

上面的例子中,我们使用循环前置语句中声明的循环变量 i 作为切片下标,逐一将切片中的元素读取了出来。不过,这样就有点麻烦了。但是使用for range 循环后如下:

var sl = []int{1, 2, 3, 4, 5}
for i, v := range sl {
fmt.Printf("sl[%d] = %d\n", i, v)
}

我们看到,for range 循环形式除了循环体保留了下来,其余组成部分都“不见”了。其实那几部分已经被融合到 for range 的语义中了

具体来说,这里的 i v 对应的是for 语句形式中循环前置语句的循环变量,它们的初值分别为切片 sl 的第一个元素的下标值和元素值。并且,隐含在 for range 语义中的循环控制条件判断为:是否已经遍历完 sl 的所有元素,等价于i < len(sl)这个布尔表达式。另外,每次迭代后,for range 会取出切片 sl 的下一个元素的下标和值,分别赋值给循环变量 i v,这与 for 经典形式下的循环后置语句执行的逻辑是相同的。

3.3 for range 语句几个常见的“变种”

3.3.1 省略value

有时候,您可能只对元素中的index感兴趣,而不需要值value。在这种情况下,您可以省略值部分,只使用键。示例如下:

fruits := []string{"apple", "banana", "cherry"}
for index := range fruits {
fmt.Printf("Index: %d\n", index)
}

3.3.2 省略 key

如果我们不关心元素下标,只关心元素值,那么我们可以用空标识符替代代表下标值的变量 i。这里一定要注意,这个空标识符不能省略,否则就与上面形式一样了,Go 编译器将无法区分:

for _, v := range sl {
// ...
}

3.3.3 同时省略 key 和 value

如果我们既不关心元素下标值,也不关心元素值,那是否能写成下面这样呢:

for _, _ = range sl {
// ...
}

这种形式在语法上没有错误,就是看起来不太优雅。Go 在Go 1.4 版本中就提供了一种优雅的等价形式,后续直接使用这种形式就好了:

for range sl {
// ...
}

四、for 循环常用操作

4.1 遍历数组、切片——获得索引和元素

在遍历代码中,key 和 value 分别代表切片的下标及下标对应的值。下面的代码展示如何遍历切片,数组也是类似的遍历方法:

package main

import "fmt"

func main() {
for key, value := range []int{1, 2, 3, 4} {
fmt.Printf("key:%d value:%d\n", key, value)
}
}
/*
代码输出如下:
key:0 value:1
key:1 value:2
key:2 value:3
key:3 value:4
*/

4.2 遍历string 类型--获得字符串

下面这段代码展示了如何遍历字符串:

var s = "中国人"
for i, v := range s {
fmt.Printf("%d %s 0x%x\n", i, string(v), v)
}

输出结果如下:

0 中 0x4e2d
3 国 0x56fd
6 人 0x4eba

我们看到:for range 对于 string 类型来说,每次循环得到的 v 值是一个 Unicode 字符码点,也就是 rune 类型值,而不是一个字节,返回的第一个值 i 为该 Unicode 字符码点的内存编码(UTF-8)的第一个字节在字符串内存序列中的位置。

遍历map——获得map的键和值

map 就是一个键值对(key-value)集合,最常见的对 map 的操作,就是通过 key 获取其对应的 value 值。但有些时候,我们也要对 map 这个集合进行遍历,这就需要 for 语句的支持了。

但在 Go 语言中,我们要对 map 进行循环操作,for range 是唯一的方法,for 经典循环形式是不支持对 map 类型变量的循环控制的。下面是通过 for range,对一个 map 类型变量进行循环操作的示例:

var m = map[string]int {
"Rob" : 67,
"Russ" : 39,
"John" : 29,
} for k, v := range m {
println(k, v)
}

运行这个示例,我们会看到这样的输出结果:

John 29
Rob 67
Russ 39

通过输出结果我们看到:for range 对于 map 类型来说,每次循环,循环变量 k 和 v 分别会被赋值为 map 键值对集合中一个元素的 key 值和 value。而且,map 类型中没有下标的概念,通过 keyvalue 来循环操作 map 类型变量也就十分自然了。

遍历通道(channel)——接收通道数据

除了可以针对 string、数组 / 切片,以及 map 类型变量进行循环操作控制之外,for range 还可以与 channel 类型配合工作。

c := make(chan int)
go func() {
c <- 1
c <- 2
c <- 3
close(c)
}()
for v := range c {
fmt.Println(v)
}

channel 是 Go 语言提供的并发设计的原语,它用于多个 Goroutine 之间的通信。当 channel 类型变量作为 for range 语句的迭代对象时,for range 会尝试从 channel 中读取数据,使用形式是这样的:

var c = make(chan int)
for v := range c {
// ...
}

在这个例子中,for range 每次从 channel 中读取一个元素后,会把它赋值给循环变量 v,并进入循环体。当 channel 中没有数据可读的时候, for range 循环会阻塞在对 channel 的读操作上。直到 channel 关闭时,for range 循环才会结束,这也是 for range 循环与 channel 配合时隐含的循环判断条件。

五、跳出循环与终止循环

5.1 continue 语句(继续下次循环)

5.1.1 continue 基本语法

首先,我们来看第一种场景。如果循环体中的代码执行到一半,要中断当前迭代,忽略此迭代循环体中的后续代码,并回到 for 循环条件判断,尝试开启下一次迭代,这个时候我们可以怎么办呢?我们可以使用 continue 语句来应对。基本语法如下:

for initialization; condition; update {
// 循环体
if someCondition {
continue
}
// 其他循环体的代码
}
  • initialization 是初始化语句,通常用于初始化循环变量。
  • condition 是循环条件,当条件为真时继续循环,否则退出。
  • update 是在每次迭代后执行的操作,通常用于更新循环变量。

带标签的 continue 语句用于跳过当前迭代中 if 语句中的 someCondition 满足的部分,直接进行下一次迭代。如果没有标签,continue 将默认跳过当前循环的下一次迭代。

以下是一个示例,演示 continue 语句的基本语法:

var sum int
var sl = []int{1, 2, 3, 4, 5, 6}
for i := 0; i < len(sl); i++ {
if sl[i]%2 == 0 {
// 忽略切片中值为偶数的元素
continue
}
sum += sl[i]
}
println(sum) // 9

这段代码会循环遍历切片中的元素,把值为奇数的元素相加,然后存储在变量 sum 中。我们可以看到,在这个代码的循环体中,如果我们判断切片元素值为偶数,就使用 continue 语句中断当前循环体的执行,那么循环体下面的 sum += sl[i] 在这轮迭代中就会被忽略。代码执行流会直接来到循环后置语句i++,之后对循环条件表达式(i < len(sl))进行求值,如果为 true,将再次进入循环体,开启新一次迭代。

5.1.2 带标签的continue语句

Go 语言中的 continue 在 C 语言 continue 语义的基础上又增加了对 label 的支持。label 语句的作用,是标记跳转的目标。带标签的continue语句的基本语法格式如下:

loopLabel:
for initialization; condition; update {
// 循环体
if someCondition {
continue loopLabel
}
}
  • loopLabel 是一个用户定义的标签(标识符),用于标记循环。
  • initialization 是初始化语句,通常用于初始化循环变量。
  • condition 是循环条件,当条件为真时继续循环,否则退出。
  • update 是在每次迭代后执行的操作,通常用于更新循环变量。

带标签的continue语句用于在嵌套循环中指定要跳过的循环,其工作方式是:如果某个条件满足,执行continue loopLabel,其中loopLabel是要跳过的循环的标签,它将控制流转移到带有相应标签的循环的下一次迭代。如果没有指定标签,continue将默认跳过当前循环的下一次迭代。

我们可以把上面的代码改造为使用 label 的等价形式:

func main() {
var sum int
var sl = []int{1, 2, 3, 4, 5, 6} loop:
for i := 0; i < len(sl); i++ {
if sl[i]%2 == 0 {
// 忽略切片中值为偶数的元素
continue loop
}
sum += sl[i]
}
println(sum) // 9
}

你可以看到,在这段代码中,我们定义了一个 label:loop,它标记的跳转目标恰恰就是我们的 for 循环。也就是说,我们在循环体中可以使用continue+ loop label的方式来实现循环体中断,这与前面的例子在语义上是等价的。不过这里仅仅是一个演示,通常我们在这样非嵌套循环的场景中会直接使用不带 label 的 continue 语句。

而带 label 的 continue 语句,通常出现于嵌套循环语句中,被用于跳转到外层循环并继续执行外层循环语句的下一个迭代,比如下面这段代码:

func main() {
var sl = [][]int{
{1, 34, 26, 35, 78},
{3, 45, 13, 24, 99},
{101, 13, 38, 7, 127},
{54, 27, 40, 83, 81},
} outerloop:
for i := 0; i < len(sl); i++ {
for j := 0; j < len(sl[i]); j++ {
if sl[i][j] == 13 {
fmt.Printf("found 13 at [%d, %d]\n", i, j)
continue outerloop
}
}
}
}

在这段代码中,变量 sl 是一个元素类型为[]int 的切片(二维切片),其每个元素切片中至多包含一个整型数 13。main 函数的逻辑就是在 sl 的每个元素切片中找到 13 这个数字,并输出它的具体位置信息。

那这要怎么查找呢?一种好的实现方式就是,我们只需要在每个切片中找到 13,就不用继续在这个切片的剩余元素中查找了。

我们用 for 经典形式来实现这个逻辑。面对这个问题,我们要使用嵌套循环,具体来说就是外层循环遍历 sl 中的元素切片,内层循环遍历每个元素切片中的整型值。一旦内层循环发现 13 这个数值,我们便要中断内层 for 循环,回到外层 for 循环继续执行。

如果我们用不带 label 的 continue 能不能完成这一功能呢?答案是不能。因为它只能中断内层循环的循环体,并继续开启内层循环的下一次迭代。而带 label 的 continue 语句是这个场景下的“最佳人选”,它会直接结束内层循环的执行,并回到外层循环继续执行。

这一行为就好比在外层循环放置并执行了一个不带labelcontinue 语句。它会中断外层循环中当前迭代的执行,执行外层循环的后置语句(i++),然后再对外层循环的循环控制条件语句进行求值,如果为 true,就将继续执行外层循环的新一次迭代。

5.2 goto(跳转到指定标签)

goto语句通过标签进行代码间的无条件跳转。goto语句可以在快速跳出循环、避免重复退出上有一定的帮助。Go语言中使用goto语句能简化一些代码的实现过程。 例如双层嵌套的for循环要退出时:

func main() {
var breakFlag bool
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if j == 2 {
// 设置退出标签
breakFlag = true
break
}
fmt.Printf("%v-%v\n", i, j)
}
// 外层for循环判断
if breakFlag {
break
}
}
}

使用goto语句能简化代码:

func main() {
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if j == 2 {
// 设置退出标签
goto breakTag
}
fmt.Printf("%v-%v\n", i, j)
}
}
return
// 标签
breakTag:
fmt.Println("结束for循环")
}

goto 是一种公认的、难于驾驭的语法元素,应用 goto 的代码可读性差、代码难于维护还易错。虽然 Go 语言保留了 goto,在平常开发中,不推荐使用。

5.3 break(跳出循环)

日常编码中,我们还会遇到一些场景,在这些场景中,我们不仅要中断当前循环体迭代的进行,还要同时彻底跳出循环,终结整个循环语句的执行。面对这样的场景,continue 语句就不再适用了,Go 语言为我们提供了 break 语句来解决这个问题。

5.3.1 break基本语法

break语句的基本语法如下:

for initialization; condition; update {
// 循环体
if someCondition {
break
}
// 其他循环体的代码
}
  • initialization 是初始化语句,通常用于初始化循环变量。
  • condition 是循环条件,当条件为真时继续循环,否则退出。
  • update 是在每次迭代后执行的操作,通常用于更新循环变量。

当在循环中执行 break 语句时,它会立即终止当前的循环,无论条件是否满足,然后将控制流传递到循环之后的代码。

我们来看下面这个示例中 break 语句的应用:

func main() {
var sl = []int{5, 19, 6, 3, 8, 12}
var firstEven int = -1 // 找出整型切片sl中的第一个偶数
for i := 0; i < len(sl); i++ {
if sl[i]%2 == 0 {
firstEven = sl[i]
break
}
} println(firstEven) // 6
}

这段代码逻辑很容易理解,我们通过一个循环结构来找出切片 sl 中的第一个偶数,一旦找到就不需要继续执行后续迭代了。这个时候我们就通过 break 语句跳出了这个循环。

5.3.2 带标签的break语法

continue 语句一样,Go 也 break 语句增加了对 label 的支持。而且,和前面 continue 语句一样,如果遇到嵌套循环,break 要想跳出外层循环,用不带 label 的 break 是不够,因为不带 labelbreak 仅能跳出其所在的最内层循环。要想实现外层循环的跳出,我们还需给 break 加上 label。所以,带标签的 break 语句允许您从嵌套循环中跳出特定循环,而不是默认跳出当前循环。带标签的 break 语法如下:

loopLabel:
for initialization; condition; update {
// 循环体
if someCondition {
break loopLabel
}
// 其他循环体的代码
}
  • loopLabel 是用户定义的标签(标识符),用于标记循环。
  • initialization 是初始化语句,通常用于初始化循环变量。
  • condition 是循环条件,当条件为真时继续循环,否则退出。
  • update 是在每次迭代后执行的操作,通常用于更新循环变量。

当带标签的 break 语句执行时,它会终止带有相应标签的循环,而不是默认的当前循环。

我们来看一个具体的例子:

var gold = 38

func main() {
var sl = [][]int{
{1, 34, 26, 35, 78},
{3, 45, 13, 24, 99},
{101, 13, 38, 7, 127},
{54, 27, 40, 83, 81},
} outerloop:
for i := 0; i < len(sl); i++ {
for j := 0; j < len(sl[i]); j++ {
if sl[i][j] == gold {
fmt.Printf("found gold at [%d, %d]\n", i, j)
break outerloop
}
}
}
}

这个例子和我们前面的带 label 的 continue 语句的例子很像,main 函数的逻辑就是,在 sl 这个二维切片中找到 38 这个数字,并输出它的位置信息。整个二维切片中至多有一个值为 38 的元素,所以只要我们通过嵌套循环发现了 38,我们就不需要继续执行这个循环了。这时,我们通过带有 label 的 break 语句,就可以直接终结外层循环,从而从复杂多层次的嵌套循环中直接跳出,避免不必要的算力资源的浪费。

六、for 循环常见“坑”与避坑指南

for 语句的常见“坑”点通常和 for range 这个“语法糖”有关。虽然 for range 的引入提升了 Go 语言的表达能力,也简化了循环结构的编写,但 for range 也不是“免费的午餐”,在开发中,经常会遇到一些问题,下面我们就来看看这些常见的问题。

6.1 循环变量的重用

我们前面说过,for range 形式的循环语句,使用短变量声明的方式来声明循环变量,循环体将使用这些循环变量实现特定的逻辑,但你在刚开始学习使用的时候,可能会发现循环变量的值与你之前的“预期”不符,比如下面这个例子:

func main() {
var m = []int{1, 2, 3, 4, 5} for i, v := range m {
go func() {
time.Sleep(time.Second * 3)
fmt.Println(i, v)
}()
} time.Sleep(time.Second * 10)
}

这个示例是对一个整型切片进行遍历,并且在每次循环体的迭代中都会创建一个新的 Goroutine(Go 中的轻量级协程),输出这次迭代的元素的下标值与元素值。

现在我们继续看这个例子,我们预期的输出结果可能是这样的:

0 1
1 2
2 3
3 4
4 5

那实际输出真的是这样吗?我们实际运行输出一下:

4 5
4 5
4 5
4 5
4 5

我们看到,Goroutine 中输出的循环变量,也就是 i 和 v 的值都是 for range 循环结束后的最终值,而不是各个 Goroutine 启动时变量 i 和 v 的值,与我们最初的“预期”不符,这是为什么呢?

这是因为我们最初的“预期”本身就是错的。这里,很可能会被 for range 语句中的短声明变量形式“迷惑”,简单地认为每次迭代都会重新声明两个新的变量 i 和 v。但事实上,这些循环变量在 for range 语句中仅会被声明一次,且在每次迭代中都会被重用。

基于隐式代码块的规则,我们可以将上面的 for range 语句做一个等价转换,这样可以帮助你理解 for range 的工作原理。等价转换后的结果是这样的:

func main() {
var m = []int{1, 2, 3, 4, 5} {
i, v := 0, 0
for i, v = range m {
go func() {
time.Sleep(time.Second * 3)
fmt.Println(i, v)
}()
}
} time.Sleep(time.Second * 10)
}

通过等价转换后的代码,我们可以清晰地看到循环变量 i 和 v 在每次迭代时的重用。而 Goroutine 执行的闭包函数引用了它的外层包裹函数中的变量 i、v,这样,变量 i、v 在主 Goroutine 和新启动的 Goroutine 之间实现了共享,而 i, v 值在整个循环过程中是重用的,仅有一份。在 for range 循环结束后,i = 4, v = 5,因此各个 Goroutine 在等待 3 秒后进行输出的时候,输出的是 i, v 的最终值。

那么如何修改代码,可以让实际输出和我们最初的预期输出一致呢?我们可以为闭包函数增加参数,并且在创建 Goroutine 时将参数与 i、v 的当时值进行绑定,看下面的修正代码:

func main() {
var m = []int{1, 2, 3, 4, 5} for i, v := range m {
go func(i, v int) {
time.Sleep(time.Second * 3)
fmt.Println(i, v)
}(i, v)
} time.Sleep(time.Second * 10)
}

这回的输出结果与我们的预期就是一致的了。不过这里你要注意:你执行这个程序的输出结果的行序,可能与我的不同,这是由 Goroutine 的调度所决定的。

6.2 参与循环的是 range 表达式的副本

在 for range 语句中,range 后面接受的表达式的类型可以是数组、指向数组的指针、切片、字符串,还有 map 和 channel(需具有读权限)。我们以数组为例来看一个简单的例子:

func main() {
var a = [5]int{1, 2, 3, 4, 5}
var r [5]int fmt.Println("original a =", a) for i, v := range a {
if i == 0 {
a[1] = 12
a[2] = 13
}
r[i] = v
} fmt.Println("after for range loop, r =", r)
fmt.Println("after for range loop, a =", a)
}

这个例子说的是对一个数组 a 的元素进行遍历操作,当处理下标为 0 的元素时,我们修改了数组 a 的第二个和第三个元素的值,并且在每个迭代中,我们都将从 a 中取得的元素值赋值给新数组 r。我们期望这个程序会输出如下结果:

original a = [1 2 3 4 5]
after for range loop, r = [1 12 13 4 5]
after for range loop, a = [1 12 13 4 5]

但实际运行该程序的输出结果却是:

original a = [1 2 3 4 5]
after for range loop, r = [1 2 3 4 5]
after for range loop, a = [1 12 13 4 5]

我们原以为在第一次迭代过程,也就是 i = 0 时,我们对 a 的修改 (a[1] =12,a[2] = 13) 会在第二次、第三次迭代中被 v 取出,但从结果来看,v 取出的依旧是 a 被修改前的值:2 和 3。

为什么会是这种情况呢?原因就是参与 for range 循环的是 range 表达式的副本。也就是说,在上面这个例子中,真正参与循环的是 a 的副本,而不是真正的 a。

为了方便你理解,我们将上面的例子中的 for range 循环,用一个等价的伪代码形式重写一下:

for i, v := range a' { //a'是a的一个值拷贝
if i == 0 {
a[1] = 12
a[2] = 13
}
r[i] = v
}

现在真相终于揭开了:这个例子中,每次迭代的都是从数组 a 的值拷贝 a’中得到的元素。a’是 Go 临时分配的连续字节序列,与 a 完全不是一块内存区域。因此无论 a 被如何修改,它参与循环的副本 a’依旧保持原值,因此 v 从 a’中取出的仍旧是 a 的原值,而不是修改后的值。

那么应该如何解决这个问题,让输出结果符合我们前面的预期呢?在 Go 中,大多数应用数组的场景我们都可以用切片替代,这里我们也用切片来试试看:

func main() {
var a = [5]int{1, 2, 3, 4, 5}
var r [5]int fmt.Println("original a =", a) for i, v := range a[:] {
if i == 0 {
a[1] = 12
a[2] = 13
}
r[i] = v
} fmt.Println("after for range loop, r =", r)
fmt.Println("after for range loop, a =", a)
}

你可以看到,在 range 表达式中,我们用了 a[:]替代了原先的 a,也就是将数组 a 转换为一个切片,作为 range 表达式的循环对象。运行这个修改后的例子,结果是这样的:

original a = [1 2 3 4 5]
after for range loop, r = [1 12 13 4 5]
after for range loop, a = [1 12 13 4 5]

我们看到输出的结果与最初的预期终于一致了,显然用切片能实现我们的要求。

那切片是如何做到的呢?切片在 Go 内部表示为一个结构体,由(array, len, cap)组成,其中 array 是指向切片对应的底层数组的指针,len 是切片当前长度,cap 为切片的最大容量。

所以,当进行 range 表达式复制时,我们实际上复制的是一个切片,也就是表示切片的结构体。表示切片副本的结构体中的 array,依旧指向原切片对应的底层数组,所以我们对切片副本的修改也都会反映到底层数组 a 上去。而 v 再从切片副本结构体中 array 指向的底层数组中,获取数组元素,也就得到了被修改后的元素值。

6.3 遍历 map 中元素的随机性

根据上面的讲解,当 map 类型变量作为 range 表达式时,我们得到的 map 变量的副本与原变量指向同一个 map。如果我们在循环的过程中,对 map 进行了修改,那么这样修改的结果是否会影响后续迭代呢?这个结果和我们遍历 map 一样,具有随机性。

比如我们来看下面这个例子,在 map 循环过程中,当 counter 值为 0 时,我们删除了变量 m 中的一个元素:

var m = map[string]int{
"tony": 21,
"tom": 22,
"jim": 23,
} counter := 0
for k, v := range m {
if counter == 0 {
delete(m, "tony")
}
counter++
fmt.Println(k, v)
}
fmt.Println("counter is ", counter)

如果我们反复运行这个例子多次,会得到两个不同的结果。当 k="tony"作为第一个迭代的元素时,我们将得到如下结果:

tony 21
tom 22
jim 23
counter is 3

否则,我们得到的结果是这样的:

tom 22
jim 23
counter is 2

如果我们在针对 map 类型的循环体中,新创建了一个 map 元素项,那这项元素可能出现在后续循环中,也可能不出现:

var m = map[string]int{
"tony": 21,
"tom": 22,
"jim": 23,
} counter := 0
for k, v := range m {
if counter == 0 {
m["lucy"] = 24
}
counter++
fmt.Println(k, v)
}
fmt.Println("counter is ", counter)

这个例子的执行结果也会有两个:

tony 21
tom 22
jim 23
lucy 24
counter is 4

或:

tony 21
tom 22
jim 23
counter is 3

考虑到上述这种随机性,我们日常编码遇到遍历 map 的同时,还需要对 map 进行修改的场景的时候,要格外小心。

Go 循环之for循环,仅此一种的更多相关文章

  1. GO学习笔记 - Go 只有一种循环结构—— for 循环。

    一,Go 只有一种循环结构—— for 循环. 官方教程:https://tour.go-zh.org/flowcontrol/1 Go 只有一种循环结构—— for 循环. 基本的 for 循环包含 ...

  2. Python之for循环和while循环

    Python中有两种循环,分别为:for 循环和 while 循环. 循环语句就是在某种条件下,循环的执行某段代码块,并在符合条件的情况下跳出该段循环, 其目的是处理想要进行处理的相同任务.它的关键词 ...

  3. python 循环高级用法 [expression for x in X [if condition] for y in Y [if condition] ... for n in N [if condition] ]按照从左至右的顺序,分别是外层循环到内层循环

    高级语法 除了像上面介绍的 [x ** 2 for x in L] 这种基本语法之外,列表推导式还有一些高级的扩展. 4.1. 带有if语句 我们可以在 for 语句后面跟上一个 if 判断语句,用于 ...

  4. sqlserver中的循环遍历(普通循环和游标循环)

    sql 经常用到循环,下面介绍一下普通循环和游标循环 1.首先需要一个测试表数据Student

  5. while 循环 于 for循环语句

    while 循环 while 循环 while 条件: 如果条件是真的,就继续的循环 如果条件是假的,就停止循环 循环的意思就是让程序重复地执行某些语句,whiler循环就是循环结构的一种,当事先不知 ...

  6. 1. while循环(当循环) 2. do{}while()循环 3. switch cose(多选一) 例子:当选循环下求百鸡百钱 用 switch cose人机剪刀石头布

    1. while循环: 当选循环下求百鸡百钱:如下: 代码: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN ...

  7. iOS - OC RunLoop 运行循环/消息循环

    1.RunLoop 1)运行循环: 运行循环在 iOS 开发中几乎不用,但是概念的理解却非常重要. 同一个方法中的代码一般都在同一个运行循环中执行,运行循环监听 UI 界面的修改事件,待本次运行循环结 ...

  8. 黑马程序员——JAVA基础之程序控制流结构之循环结构,循环嵌套

    ------- android培训.java培训.期待与您交流! ---------- 循环结构: 代表语句:while ,do while ,for while语句格式 : while(条件表达式) ...

  9. python 循环、循环设计、循环对象

    作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! 1.循环 循环用于重复执行一些程序块.从上一讲的选择结构,我们已经看到了如何用缩进 ...

  10. PHP foreach()跳出本次或当前循环与终止循环方法

    PHPforeach()跳出本次或当前循环与终止循环方法 PHP中用foreach()循环中,想要在循环的时候,当满足某个条件时,想 $arr = array('a','b','c','d','e') ...

随机推荐

  1. Codeforces Round #738 (Div. 2) (A~E)

    比赛链接:Here 1559A. Mocha and Math 题意: 给定一个区间,选择区间内的值执行 & 操作使得区间最大值最小化 观察样例发现:令 x = (1 << 30) ...

  2. 通义千问预体验,如何让 AI 模型应用“奔跑”在函数计算上?

    立即体验基于函数计算部署通义千问预体验: https://developer.aliyun.com/topic/aigc_fc AIGC 浪潮已来,从文字生成到图片生成,AIGC 的创造力让人惊叹,更 ...

  3. Serverless实战——2分钟,教你用Serverless每天给女朋友自动发土味情话

    原文链接:https://developer.aliyun.com/article/981268 演示视频:https://developer.aliyun.com/live/249772 一.Ser ...

  4. Spring Boot Serverless 实战系列“部署篇” | Mall 应用

    导读:SpringBoot 是基于 Java Spring 框架的套件,它预装了 Spring 的一系列组件,让开发者只需要很少的配置就可以创建独立运行的应用程序.在云原生的世界里,有大量的平台可以运 ...

  5. CPLEX通过Python API获取Gap值的方法

    写在前面 最近在使用Cplex求解模型,尽管Cplex的Python API会自动输出引擎日志,但在多次求解中一次次看引擎日志找Gap值并做实验记录很麻烦,所以需要找到获取Gap值的方法.然而我在Cp ...

  6. Linux一键安装docker脚本,含ubuntu和centos

    将脚本保存为docker_install.sh,可以上传git,到服务器中,git clone下来后,sh docker_install.sh即可自动安装 ubuntu实现 # 以Ubuntu为例 # ...

  7. 启动vue项目失败,报错Failed at the node-sass@4.14.1 postinstall script.

    https://www.cnblogs.com/xiaodangshan/p/13061618.html

  8. React技巧之设置input值

    原文链接:https://bobbyhadz.com/blog/react-set-input-value-on-button-click 作者:Borislav Hadzhiev 正文从这开始~ 总 ...

  9. Hive(2)-Hive安装及简单使用

    本文的安装版本为Hive 3.1.2,且安装为单节点. 1. 安装参考及注意事项 (1) 官网:http://hive.apache.org/ (2) 上篇博客:Hadoop 3.1.3伪分布式环境安 ...

  10. web - 解决 formdata 打印空对象

    获取单个值可以使用formData对象.get();而直接打印是看不到的.因为外界访问不到,你使用append方法后,对应的键值对就已经添加到表单里面了,你在控制台看到的是FormData原型,存储的 ...