一、原因
在 Go 中,循环变量的作用域是整个 for 循环语句块。因此,循环变量在 for 循环语句块中的代码都是可见的。
但是,当循环变量的值被用于闭包, 协程或者使用指针类型的数据结构时,会出现一些问题。这是因为循环变量的值在每一次迭代中都会被重新赋值,所以闭包或指针可能会看到循环变量的最后一个值。
二、情况分析
一般说来,循环变量作用域的问题主要出现在以下几种情况:
1. 循环变量被使用在闭包中
循环变量被闭包中使用是一个常见的问题,下面是一些示例代码:
- 访问循环变量 i 的闭包:
var arr []*int
for i := 0; i < 3; i++ {arr = append(arr, &i)
}
for _, f := range arr {fmt.Print(*f)
}
输出结果为 “333”,因为闭包访问的是循环变量的地址,而不是值。
- 在 for 循环中自增一个变量 j,然后将其作为参数传递给一个闭包:
for i := 0; i < 3; i++ {j := igo func() {fmt.Print(j)}()
}
输出结果是 “012”,这是因为闭包捕获的是 j 的值,每个goroutine都会访问自己的 j 副本。
- 在使用defer关键字的时候,会出现和闭包一样的问题。
for i := 0; i < 3; i++ {defer fmt.Print(i)
}
这个代码的输出结果是 “321”,因为 defer 语句在函数返回前执行,所以闭包捕获的都是同一个循环变量的地址。
- 在使用函数字面量的时候,同样会出现和闭包一样的问题。
func main() {fns := make([]func(), 0)for i := 0; i < 3; i++ {fns = append(fns, func() {fmt.Print(i)})}for _, f := range fns {f()}
}
输出结果为 “333”,这是因为函数字面量捕获的是变量 i 的地址,而 i 的值在循环结束后一直是 3。
2. 循环变量被使用在指针类型数据结构中
循环变量被使用在指针类型数据结构中也是一个常见的问题,下面是一些示例代码:
- 访问循环变量 i 的指针类型数据结构:
var arr []*int
for i := 0; i < 3; i++ {arr = append(arr, &i)
}
for _, p := range arr {fmt.Print(*p)
}
输出结果为 “333”,因为指针类型数据结构中保存的是循环变量的地址,而不是值。
- 在循环中申请一个变量 j,然后将其作为指针类型数据结构的元素:
arr := make([]*int, 0)
for i := 0; i < 3; i++ {j := iarr = append(arr, &j)
}
for _, p := range arr {fmt.Print(*p)
}
输出结果是 “012”,这是因为指针类型数据结构保存的是 j 的地址,每个元素访问的是自己所指向的值。
3.循环变量被使用在协程中
循环变量被使用在协程中是一个常见的问题,下面是一些示例代码:
- 访问循环变量 i 的协程:
for i := 0; i < 3; i++ {go func() {fmt.Print(i)}()
}
输出结果为 “333”,因为协程访问的是循环变量的地址,而不是值。
- 在循环中申请一个变量 j,然后将其作为参数传递给一个协程:
for i := 0; i < 3; i++ {j := igo func() {fmt.Print(j)}()
}
输出结果是 “012”,这是因为协程捕获的是 j 的值,每个协程都会访问自己的 j 副本。
- 在使用defer关键字的时候,同样会出现和协程一样的问题。
for i := 0; i < 3; i++ {defer fmt.Print(i)
}
这个代码的输出结果是 “321”,因为 defer 语句在函数返回前执行,所以协程捕获的都是同一个循环变量的地址。
三、解决方式
为了避免这些问题,可以在 for 循环语句块内部声明一个新的变量,以便每一次迭代都有自己的独立的值。比如,使用一个局部变量存储循环变量的值,然后在闭包或指针数据结构中使用该局部变量的值。例如:
arr := make([]*int, 0)
for i := 0; i < 3; i++ {j := i // 创建新的变量arr = append(arr, &j)
}
for _, p := range arr {fmt.Print(**p)
}
这样就可以保证每个元素都指向不同的变量,输出的结果也会更加准确。