1. 使用说明
range
应用于不同数据类型时,类似迭代器操作,返回 (索引, 值) 或 (键, 值)。 下表是对应的结构:
2. 使用示例
如果想忽略不想要的值时,可以使用_
这个特殊变量。
package mainfunc main() {s := "abc"for i := range s {// 忽略 2nd value,支持 string/array/slice/map。println(s[i])}println("***************")for _, c := range s {// 忽略 index。println(c)}println("***************")for range s {// 忽略全部返回值,仅迭代。println("*")}println("***************")m := map[string]int{"a": 1, "b": 2}for k, v := range m {// 返回 (key, value)。println(k, v)}}
当map
类型变量作为range
表达式时,我们得到的map
变量的副本与原变量指向同一个map
,如果我们在循环的过程中,对map
进行了修改,那么这样修改的结果是否会影响后续迭代呢?这个结果和我们遍历map
一样,具有随机性。
3. 常见的坑
3.1 循环变量的重用
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)}
输出结果:
4 54 54 54 54 5
预期结果
0 11 22 33 44 5
这是因为我们最初的“预期”本身就是错的。这里,初学者很可能会被for range
语句中的短声明变量形式“迷惑”,简单地认为每次迭代都会重新声明两个新的变量i
和v
。但事实上,这些循环变量在for range
语句中仅会被声明一次,且在每次迭代中都会被重用。
上面代码等价于下面
func main() {var m = []int{1, 2, 3, 4, 5} {i, v := 0, 0for 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
的最终值。
修改代码
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)}
3.2 参与循环的是 range 表达式的副本
数组是内置(build-in)类型,是一组同类型数据的集合,它是值类型,通过从 0 开始的下标索引访问元素值。在初始化后长度是固定的,无法修改其长度。
当作为方法的参数传入时将复制一份数组而不是引用同一指针。数组的长度也是其类型的一部分,通过内置函数len(array)
获取其长度。
注意,和C
中的数组相比,又是有一些不同的
Go
中的数组是值类型,换句话说,如果你将一个数组赋值给另外一个数组,那么,实际上就是将整个数组拷贝一份;如果Go
中的数组作为函数的参数,那么实际传递的参数是一份数组的拷贝,而不是数组的指针。这个和C
要区分开。因此,在Go
中如果将数组作为函数的参数传递的话,那效率就肯定没有传递指针高了;array
的长度也是Type
的一部分,这样就说明[10]int
和[20]int
是不一样的;
内置类型切片(“动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
切片中有两个概念:一是len
长度,二是cap
容量,长度是指已经被赋过值的最大下标+1,可通过内置函数len()
获得。容量是指切片目前可容纳的最多元素个数,可通过内置函数cap()
获得。
切片是引用类型,因此在当传递切片时将引用同一指针,修改值将会影响其他的对象。
需要强调的是,range
会复制对象,range
返回的是每个元素的副本,而不是直接返回对该元素的引用。
package mainimport "fmt"func main() {a := [3]int{0, 1, 2}for i, v := range a {// index、value 都是从复制品中取出。if i == 0 {// 在修改前,我们先修改原数组。a[1], a[2] = 999, 999fmt.Println(a) // 确认修改有效,输出 [0, 999, 999]。}a[i] = v + 100 // 使用复制品中取出的 value 修改原数组。}fmt.Println(a) // 输出 [100, 101, 102]。}
建议改用引用类型,其底层数据不会被复制。
package mainfunc main() {s := []int{1, 2, 3, 4, 5}for i, v := range s {// 复制 struct slice { pointer, len, cap }。if i == 0 {s = s[:3] // 对 slice 的修改,不会影响 range。s[2] = 100 // 对底层数据的修改。}println(i, v)}}
输出:
0 11 22 1003 44 5
其它示例
func main() {var a = [5]int{1, 2, 3, 4, 5}var r [5]intfmt.Println("original a =", a)for i, v := range a {if i == 0 {a[1] = 12a[2] = 13}r[i] = v}fmt.Println("after for range loop, r =", r)fmt.Println("after for range loop, a =", a)}
期望输出结果:
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]
修改循环迭代行代码为下面即可达到预期效果:
for i, v := range a[:] {# orfor i, v := range &a
如果觉得《Go 学习笔记(29)— range 作用于字符串 数组 切片 字典 通道》对你有帮助,请点赞、收藏,并留下你的观点哦!