4

Golang | 关于 for range 的一些细节

 2 years ago
source link: https://ijayer.github.io/post/tech/code/golang/20180328-%E5%85%B3%E4%BA%8E_for-range_%E7%9A%84%E4%B8%80%E4%BA%9B%E5%9D%91/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
  • for range 的迭代变量会被重用
  • range expression 的副本参与 iteration

1. iteration variable 重用

for range 的 idiomatic(惯用)使用方式是使用 short variable declaration(:=) 形式在 for expression 中声明 iteration variable,但需要注意的是这些variable在每次循环体中都会被重用,而不是重新声明。

func main() {
	var arr = [...]int{1, 2, 3, 4, 5} // 声明包含有5个int类型元素的数组

	for i, v := range arr {
		go func() {
			time.Sleep(3 * time.Second)
			fmt.Printf("index: %v, value: %v\n", i, v)
		}()
	}

	time.Sleep(10 * time.Second)
}

// output:
// i=4, v=5, 0xc042010058 0xc042010060
// i=4, v=5, 0xc042010058 0xc042010060
// i=4, v=5, 0xc042010058 0xc042010060
// i=4, v=5, 0xc042010058 0xc042010060
// i=4, v=5, 0xc042010058 0xc042010060
//
// result:
// * 从输出结果可以看到: for 循环中,开启的 go 程中 i,v 的输出值是 for...range 遍历
// 完成后的最终值,而不是 go 程启动时所传入的值
// * 其次,for...range 遍历过程中,迭代变量 i,v 始终指向同一块内存空间(i:0xc042010058,
// v:0xc042010060), 说明i,v并没有重新初始化,而是在重用

Note: 在上面的代码中,for循环启动的各个goroutine输出的 i, v 值都是 for range 循环结束后 i, v 的最终值,而不是各个goroutine启动时的 i, v 值。 一个可行的 fix 的方法:

	for i, v := range arr { // 将i,v保存在匿名函数(闭包)所声明的作用域内
		go func(i, v int) {
			time.Sleep(3 * time.Second)
			fmt.Printf("i=%v, v=%v\n", i, v)
		}(i, v)
	}
	
	// result:
	// 循环中,go 程启动时,传入的参数(i,v)会被记录在内存中,所以在go程执行时
	// 输出的 i,v 每次都是不同的值,即传入的值,且指向了不同的内存地址

2. range expression 副本参与 iteration

range 后面接收的表达式的类型包括:array、point to array、slice、map、string、channel(有读权限的)

2.1. array

func arrayRangeExpression(arr [5]int) {
	var r [5]int
	fmt.Println("a =", arr)

	for i, v := range arr { // range 'arr' is copy from arr
		if i == 0 {
			arr[1] = 12
			arr[2] = 13
		}
		r[i] = v
	}
	fmt.Println("r =", r)
	fmt.Println("a =", arr)

	// except output:
	// a = [1 2 3 4 5]
	// r = [1 12 13 4 5] 然而,这里 r 输出并不是这样的
	// a = [1 12 13 4 5]
	//
	// actual output:
	// a = [1 2 3 4 5]
	// r = [1 2 3 4 5]
	// a = [1 12 13 4 5]
	//
	// result:
	// v 是range表达式 arr 的副本 `arr`中取出来的值,因此,在 if i==0 {} 作用域内
	// 对 arr[1] arr[2]元素值做修改不会影响 r 的结果
}

Note: 我们原以为在第一次 iteration,也就是 i=0 时,我们对a的修改(arr[1]=12, arr[2]=13)会在第二次,第三次循环中被 v 取出,但结果却是v取出的依旧是a被修改前的值:2和3。这就是 for…range 的一个不大不小的坑: range expression 副本参与循环。也就是说在上面这个例子里。正真参与循环的是 arr 的副本,而不是正真的 arr。

Go 中的数组在内部表示为连续的字节序列,虽然长度是Go数组类型的一部分,但长度并不包含在数据的内部表示的部分中,而是由编译器在编译期计算出来。 这个例子中,对range表达式的拷贝,即对一个数据的拷贝,arr’ 则是Go临时分配的连续的字节序列,与 arr 完全不是一块内存。因此,无论 arr 被如何修改,其副本arr’ 依旧保持原值,并且参与循环的是arr’。所以,v从arr’中取出的仍旧是arr的原值,而非修改后的值。

Note: 但是在 for i:=0; i < len(arr); i++ {} 这类循环结构中,直接操作的是原数据的值,并不是一份拷贝,是可以修改数据值的。

2.2. pointer to array

// pointerToArrayRangeExpression
func pointerToArrayRangeExpression(arr [5]int) {
	var r [5]int

	fmt.Println("pointerToArrayRangeExpression result:")
	fmt.Println("a =", arr)

	for i, v := range &arr {
		if i == 0 {
			arr[1] = 12
			arr[2] = 13
		}
		r[i] = v
	}
	fmt.Println("r =", r)
	fmt.Println("a =", arr)
	println()
	
	// output:
	// pointerToArrayRangeExpression result:
	// a = [1 2 3 4 5]
	// r = [1 12 13 4 5]
	// a = [1 12 13 4 5]
}

Note: 我们看到这次 r 数组的值与最终a被修改后的值一致了。这个例子中使用了 *[5]int 作为 range 的表达式,其副本依旧是一个指向原始数组 arr 的指针, 因此后续所有循环中均是 &arr 指向的的原始数组的指针参与计算,因此 v 能从 &arr 指向的原始数组中取出 arr 修改后的值。

idiomatic go 建议我们尽可能的使用slice替换掉array的使用

2.3. slice

// sliceRangeExpression
func sliceRangeExpression(arr [5]int) {
	var r [5]int

	fmt.Println("sliceRangeExpression result:")
	fmt.Println("a =", arr)

	for i, v := range &arr {
		if i == 0 {
			arr[1] = 12
			arr[2] = 13
		}
		r[i] = v
	}
	fmt.Println("r =", r)
	fmt.Println("a =", arr)
	println()

	// output:
	// sliceRangeExpression result:
	// a = [1 2 3 4 5]
	// r = [1 12 13 4 5]
	// a = [1 12 13 4 5]
}

Note: 这里slice实现了预期的要求。 那 slice是如何做到的呢?

  • slice 在 go 的内部表示为一个 struct, 由 (*T, len, cap) 组成,其中 *T 指向slice对应的 underlying(底层) array的指针,len是slice当前的长度,cap是slice的最大容量。当range进行expression复制时,它实际上复制的是一个 slice, 也就是那个 struct。副本 struct 中的 *T 依旧指向原 slice 对应的 array,为此对slice的修改都反映到 underlying array arr上去了,v 从副本struct中 *T 指向的 underlying array 中获取数组元素,也就得到了修改后的元素值。

slice 与 array还有一个不同点:就是 slice 的len在运行时可以被改变,而array得len是一个常量,不可改变。那么len变化的 slice 对 for range 有何影响呢?

// sliceLenChangeRangeExpression 测试在for...range迭代过程中,切片的长度发生
// 变化将会产生什么结果
func sliceLenChangeRangeExpression(a []int) {
	var r = make([]int, 0)

	fmt.Println("sliceLenChangeRangeExpression result:")
	fmt.Println("a =", a)

	for i, v := range a {
		if i == 0 {
			a = append(a, 6, 7)
		}
		r = append(r, v)
	}
	fmt.Println("r =", r)
	fmt.Println("a =", a)
	println()

	// output:
	// liceLenChangeRangeExpression result:
	// a = [1 2 3 4 5]
	// r = [1 2 3 4 5]
	// a = [1 2 3 4 5 6 7]
}

Note: 在这个例子中,原slice a在 for range过程中被附加了两个元素6和7, 其中,len有5增加到了7,但是对于r却没有产生影响。是因为在 a 的副本 a’ 的内部表示的 struct 中,len 字段并没有改变,依旧是 5。因此 for…range只会循环5次,也就只获取a对应的underlying数组的前5个元素。

range 副本行为会带来一些性能上的消耗,尤其是当range expression的类型的为数组时,range需要复制整个数组;而当 range expression 类型为 pointer to array 或 slice时,这个消耗将小得多,仅仅需要复制一个指针或一个slice的内部表示(一个struct即可)。

2.4. 其他 range expression 类型

对于 range 后面的其他表达式类型,比如 string,map,channel,for range依旧会创建副本参与计算

string

对 string 来说,由于 string 的内部表示为 struct {*byte, len}, 并且 string 本身是 immutable(一成不变的),因此其行为和消耗和 slice expression类似。不过 for…range 对于 string 来说,每次循环的单位是 rune(code point的值),而不是byte,index为迭代字符码点的第一个字节的 position:

var s := "中国人"

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

// output:
// 0 中 0x4e2d
// 3 国 0x56fd
// 6 人 0x4eba

对于 map 来说,map 内部表示为一个指针,指针副本也指向真实的map, 因此for range操作均操作的是源map

// mapRangeExpression 测试 map 作为 for...range 的迭代变量,参与计算时将会
// 发生什么变化
func mapRangeExpression() {
	var m = map[string]int{
		"tony": 21,
		"tom":  22,
		"jim":  23,
	}
	var cnt int

	//for k, v := range m { // m' is copy from m(map[string]int)
	//	if cnt == 0 {
	//		delete(m, "tony")
	//	}
	//	cnt++
	//	fmt.Println(k, v)
	//}
	//fmt.Println("cnt:", cnt)
	//fmt.Println("---------")

	// output:
	// tony 21
	// tom 22
	// jim 23
	// cnt: 3
	//   or
	// tom 22
	// jim 23
	// cnt: 2

	cnt = 0
	for k, v := range m { // m' is copy from m(map[string]int)
		if cnt == 0 {
			m["lucy"] = 24
		}
		cnt++
		fmt.Println(k, v)
	}
	fmt.Println("cnt:", cnt)

	// output:
	// tony 21
	// tom 22
	// jim 23
	// lucy 24
	// cnt: 4
	//  or
	// tony 21
	// tom 22
	// jim 23
	// cnt: 3
}

Note:

  • 如果map中的某项在循环到达前被在循环体中删除了,那么它可能不会被iteration variable获取到
  • 如果在循环体中新建一个map元素项,那该项元素可能出现在后续循环中,也可能不出现

channel

对于 channel 来说,channel 内部表示为一个指针,channel的指针副本其实指向真是的channel

for…range最终以阻塞读的方式阻塞在channel expression上(即便是buffered channle,当channel中无数据时,for…range也会阻塞在channel),直到channel关闭

func channelRangeExpression() {
	var c = make(chan int)

	go func() {
		time.Sleep(time.Second * 3)
		c <- 1
		c <- 2
		c <- 3
		close(c)
	}()

	flag := true
	start := time.Now()
	for v := range c { // 阻塞3秒,后读取数据
		if flag {
			flag = false
			end := time.Since(start)
			fmt.Println(end)
		}
		fmt.Println(v)
	}

	//output:
	//3.0004311s
	//1
	//2
	//3
}

Note: channel变量为 nil, 则for…range将永远阻塞

See Also

Thanks to the authors 🙂


Recommend

  • 83
    • 微信 mp.weixin.qq.com 6 years ago
    • Cache

    文件下载的一些安全小细节

    文件下载的一些安全小细节 Original...

  • 86

    前言一个人不会两次掉进同一个坑里,但是如果他(她)忘记了坑的位置,那就不一定了。 这篇文章记录了最近使用Golang处理JSON遇到的一些坑。 坑1号坑:omitempty的行为C#中最常用的JSON序列化类库Newtonsoft.Json中,把一个类的实例序列化成JSON,如果我们不想让某...

  • 70
    • mobile.51cto.com 6 years ago
    • Cache

    “猜画小歌”的一些细节和思考

    Quickdraw的CNN-RNN模型 "猜画小歌"用到的quickdraw模型本质上是一个分类模型,输入是笔画的点的坐标信息和每笔起始的标识信息,应用几个级联的一维卷积,再使用 BiLSTM 层并对结果进行求和,最后使用Softmax层进行分类。...

  • 64
    • studygolang.com 5 years ago
    • Cache

    关于golang中struct的一些理解

    概述 Go语言中,struct是一个非常重要的概念,它既是一种数据类型,也可以结合方法(一种特殊的函数),构建类似于OOP的类。不多说,先上 代码 : packag...

  • 35
    • www.tuicool.com 5 years ago
    • Cache

    Netty的内存管理的一些细节!

    说明 在学习Netty的时候,ByteBuf随处可见,但是如何高效分配ByteBuf还是很复杂的,Netty的池化内存分配这块还是比较难的,很多人学习过,看过但是还是云里雾里的,本篇文章就是主要来讲解: Netty分配池化的堆外内存...

  • 45
    • www.tuicool.com 4 years ago
    • Cache

    go modules 的一些细节

    前言 在一些Java的项目中,有 Maven等这些版本管理工具,可以很好的管理各种版本依赖关系,但是在 Golang 的项目中,之前官方并没有提供版本管理工具,以前都是用 go get 进行安装, 随着项目的变大, 就处理这种...

  • 37
    • 微信 mp.weixin.qq.com 4 years ago
    • Cache

    关于事务和锁的一些细节

    小贴士: 本篇文章算是回答一些同学的提问,以MySQL 5.7为例。 我们都知道 MySQL 的 InnoDB 存储引擎是支...

  • 8

    @State 基础 在 SwiftUI 中,我们使用 @State 进行私有状态管理,并驱动 View 的显示,这是基础中的基础。比如,下面的 ContentView 将在点击加号按钮时将显示的数字 +1:

  • 2

    本文首发于这里 由于笔者水平有限,文章中难免出现各种错误,欢迎吐槽。 由于篇幅所限,大部分代码细节并未讲的很清楚,如有疑问欢迎讨论。

  • 4

    ahfuzhang 旧博客:https://ahfuzhang.blogspot.com/ 公众号:一本正经的瞎扯

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK