golang学习之七:for 语句的常见“坑”与避坑方法

for循环的两种方式

for-range 常见“坑”与避坑方法

坑1:循环变量的重用

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

package mainimport ("fmt""time"
)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)
}

控制台

go run test4.go
4 5
4 5
4 5
4 5
4 5

但是以上打印结果与我们的预期不符,这是为啥。
基于golang隐式代码块的规则(大家可以自行百度,这里不在赘述),我们可以将上面的 for range 语句做一个等价转换,这样可以帮助你理解 for range 的工作原理。等价转换后的结果是这样的:

func main() {var m = []int{1, 2, 3, 4, 5}{i, v := 0, 0 // 其实代码应该是这样的,for range循环执行的时候,会在每次遍历的时候将值赋值给隐士代码块里的v,而不是重新声明一个变量vfor i, v = range m {// 由于go func() {time.Sleep(time.Second * 3)// 这里每个函数都是一个闭包且没有参数传递,所以当闭包里的代码执行的之后,闭包没有的变量它就会引用了作用域之外的变量,并且所有的协程都是睡眠3s后执行,确保for循环结束之前,所有的协程不会运行,又因为for range的循环每次都是公用的同一个变量,于是当睡眠时间过了之后,所有的i,v就都是最后一次运行时的i,v的值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 的最终值。
闭包函数:在 Golang 中,闭包是一个引用了作用域之外的变量的函数。闭包的存在时间可以超过创建它的作用域,因此它可以访问该作用域中的变量,即使在该作用域被销毁之后。

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

func main() {var m = []int{1, 2, 3, 4, 5}for i, v := range m {// 这里在闭包函数里传递了参数,所以在for循环结束之时,开始运行协程里的代码,所以每次循环传递到闭包函数里的就都是i,v的副本,又因为这里传递参数为不是指针类型,所以不受外部函数的i,v值的影响。所以每个闭包函数注册到函数栈上的都是参数的副本。当for循环完毕之后,运行的就都是每个副本的具体的值。go func(i, v int) {time.Sleep(time.Second * 3)fmt.Println(i, v)}(i, v)}time.Sleep(time.Second * 10)
}

控制台

go run test4.go
2 3
3 4
1 2
0 1
4 5

坑2:参与循环的是 range 表达式的副本

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

package mainimport ("fmt"
)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)
}

控制台

go run test4.go
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 循环,用一个等价的伪代码形式重写一
下:


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

现在真相终于揭开了:这个例子中,每次迭代的都是从数组 a 的值拷贝 a’中得到的元素。
a’是 Go 临时分配的连续字节序列,与 a 完全不是一块内存区域。因此无论 a 被如何修改,
它参与循环的副本 a’依旧保持原值,因此 v 从 a’中取出的仍旧是 a 的原值,而不是修改后
的值。
那么应该如何解决这个问题,让输出结果符合我们前面的预期呢?我们前面说过,在 Go 中,
大多数应用数组的场景我们都可以用切片替代,这里我们也用切片来试试看:

package mainimport "fmt"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)
}

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

go run test4.go
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 指向的底层数组中,获取数组元素,也就得到了被修改后的元素的值。

坑3:方法中使用for-range

我敢出一题,打赌在做的各位有一半人要写错(狗头保命)
请各位看下以下代码输出是啥

package mainimport ("fmt""time"
)type field struct {name string
}func (p *field) print() {fmt.Println(p.name)
}func main() {data1 := []*field{{"one"}, {"two"}, {"three"}}for _, v := range data1 {go v.print()}data2 := []field{{"four"}, {"five"}, {"six"}}for _, v := range data2 {go v.print()}time.Sleep(3 * time.Second)
}

控制台

go run test4.go
one
two
three
six
six
six

以上代码因为有多协程,所以输出顺序可能不尽相同,但是都有一个疑惑,那就是第二个for循环里为啥输出了六个six,而不是four、five、six?,是因为这段代码666吗?

我们来分析一下:
我们根据 Go方法的本质,也就是 一个以方法的receiver参数作为第一个参数的普通函数,对这个程序做个 等价变换。这里我们利用Method Expression方式,等价变换后的源码如下:

type field struct {name string
}func (p *field) print() {fmt.Println(p.name)
}func main() {data1 := []*field{{"one"}, {"two"}, {"three"}}for _, v := range data1 {go (*field).print(v)}data2 := []field{{"four"}, {"five"}, {"six"}}for _, v := range data2 {go (*field).print(&v)}time.Sleep(3 * time.Second)
}

由此我们看到,其实for循环里的代码go协程部分其实就是一个闭包函数
我们来分析代码

package mainimport ("fmt""time"
)type field struct {name string
}func (p *field) print() {fmt.Println(p.name)
}func main() {data1 := []*field{{"one"}, {"two"}, {"three"}}{// 隐式代码块v := objfor _, v := range data1 {// 这里相当于每次循环都调用了一个函数,并且将每个元素以参数传递进去// 然后呢,因为for-range循环的是range表达式的副本,所以这里循环的是data1的一个拷贝,但是// 因为data1里的每个元素都是指针类型的,所以这些元素里存储的都是元素对应的地址// 所以拷贝的副本相当于是一个个新的指针变量,这些新变量里存储的还是原先每个元素的地址// 相当于是新元素和旧元素都是一个地址,他们指向的是同一个内存里的东西// 又因为 Go方法的本质,也就是 一个以方法的receiver参数作为第一个参数的普通函数注意此处(方法的reciver就是*field而非field,这点很重要)// 所以go v.print()等价于go (*field).print(v),其中v是指针类型(因为方法的reciver就是*field而非field,这点很重要)// 好了所有的条件都清楚了,那么我们分析每一次for循环的逻辑// 第一次for循环,第一个元素在隐式代码块里将值赋值给了循环变量v,然后创建协程,将v作为函数的参数注册到函数栈上(其实就是元素的地址注册到了函数栈上,又因为go所有函数/方法传参都是拷贝副本传参数,但是此处的参数是一个指针类型,那么拷贝的新变量是一个指针类型的变量它指向的还是原先元素的地址,所以每次注册在函数上的都是不同的地址)// 第二次for循环,第二个元素在隐式代码块里将值赋值给了循环变量v,然后创建协程,将v作为函数的参数注册到函数栈上(其实就是元素的地址注册到了函数栈上,又因为go所有函数/方法传参都是拷贝副本传参数,但是此处的参数是一个指针类型,那么拷贝的新变量是一个指针类型的变量它指向的还是原先元素的地址,所以每次注册在函数上的都是不同的地址)// 第三次for循环,第三个元素在隐式代码块里将值赋值给了循环变量v,然后创建协程,将v作为函数的参数注册到函数栈上(其实就是元素的地址注册到了函数栈上,又因为go所有函数/方法传参都是拷贝副本传参数,但是此处的参数是一个指针类型,那么拷贝的新变量是一个指针类型的变量它指向的还是原先元素的地址,所以每次注册在函数上的都是不同的地址)// 然后等到函数在函数栈上运行的时候,每个print函数打印的就都是分别不同的元素的name字段的值,于是输出的就是one、two、threego (*field).print(v)}}data2 := []field{{"four"}, {"five"}, {"six"}}for _, v := range data2 {// 这里相当于每次循环都调用了一个函数,并且将每个元素以参数传递进去// 然后呢,因为for-range循环的是range表达式的副本,所以这里循环的是data1的一个拷贝,但是// 因为data2里的每个元素都是非指针类型的,所以每个元素就都是一个新的元素与data2的元素就没有关联了// 又因为 Go方法的本质,也就是 一个以方法的receiver参数作为第一个参数的普通函数,(因为方法的reciver就是*field而非field,这点很重要)// 但是我们的元素都是非指针类型的,所以这里要传递指针类型的参数才可以,go帮我们隐士转换了// 所以 go v.print()等价于 (*field).print(&v),本质上就是一个普通的函数,参数为指针类型的field,其中v是指针类型(因为方法的reciver就是*field而非field,这点很重要)// 好了所有的条件都清楚了,那么我们分析每一次for循环的逻辑// 第一次for循环,第一个元素在隐式代码块里将值赋值给了循环变量v,然后创建协程, 将v作为函数的参数注册到函数栈上(其实就是元素的地址注册到了函数栈上,// 但是函数要求参数必须是指针类型的(reciver为函数的第一个参数),所以这里我们要传递指针进去所以取的就是循环变量的v的值,// 但是此处数组元素都为非指针,所以每次循环都只是将元素的值赋给了v,v的地址并没有发生变化,然后函数传参时传递的是参数的拷贝,但是此处参数是个指针类型,拷贝出来的参数也是一个指针类型,它指向的v的地址,相当于3次for循环注册到函数上的是同一个地址// 所以3次for循环里通过go调用print函数传递的都是同一个v对象,又因为go协程为异步,所以for循环完毕之后才执行了协程,然后执行的之后就是for循环最后一个元素的值,即six,six,six)(所以这里如果不是异步,是同步,那么你在每次for循环里让协程sleep 1秒,那么输出的就是4,5,6了)// 然后等到函数在函数栈上运行的时候,每个print函数打印的就是最后一次循环的v的值,于是输出的就是six、six、sixgo (*field).print(&v)}time.Sleep(3 * time.Second)
}

那么还有问题,怎么让第二个for打印4,5,6呢?
其实,我们只需要将field类型print方法的receiver类型由*field改为field就可以了。我们直接来看一下修改后的代码:

type field struct {name string
}func (p field) print() {fmt.Println(p.name)
}func main() {data1 := []*field{{"one"}, {"two"}, {"three"}}for _, v := range data1 {go v.print()}data2 := []field{{"four"}, {"five"}, {"six"}}for _, v := range data2 {go v.print()}time.Sleep(3 * time.Second)
}

为啥将方法print的receiver由指针类型改为非指针类型就可以了呢?
我们简单分析一下

package mainimport ("fmt""time"
)type field struct {name string
}func (p field) print() {fmt.Println(p.name)
}func main() {data1 := []*field{{"one"}, {"two"}, {"three"}}for _, v := range data1 {go v.print()}data2 := []field{{"four"}, {"five"}, {"six"}}for _, v := range data2 {// 这里不管切片里的元素是不是指针类型,也不管for-range拷贝的副本里的元素是不是指针类型// 在每次for循环的时候,调用的都是v.print()方法,相当于是 在函数栈上注册了一个这样的函数// 参数为一个field类型的函数,于是每次for循环注册在函数栈上的都直接是for的每次循环的元素的值,就是1,2,3,4,5,6// 然后go的所有函数/方法传参,没有引用一说,全部都是拷贝参数的副本传参,不管拷贝多少次,这里都是非指针类型// 所以打印出来的当然就是每次元素的值。go v.print()}time.Sleep(3 * time.Second)
}

总结

1.凡是用到for-range的地方一定要小心
2.凡是用到闭包的地方,参数取值或传参一定要小心
3.go方法的本质,也就是 一个以方法的receiver参数作为第一个参数的普通函数
4.for-range的时候,如果调用闭包千千万万要看清在将函数注册到函数栈上时有没有注册参数:
若有:则注意参数的类型是指针还是非指针。
若没有:那么函数运行时,就找闭包函数之外的变量运行了,注意此时变量的值。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/6451.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OpenCV自带的HAAR级联分类器对脸部(人脸、猫脸等)的检测识别

在计算机视觉领域,检测人脸等是一种很常见且非常重要的应用,我们可以先通过开放计算机视觉库OpenCV来熟悉这个人脸识别领域。另外OpenCV关于颜色的识别,可以查阅:OpenCV的HSV颜色空间在无人车中颜色识别的应用HSV颜色识别的跟踪实…

苹果账号被禁用怎么办

转载:苹果账号被禁用怎么办 目录 禁用的原因 解除Apple ID禁用 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UKQ1ILhC-1689932607373)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw)]​编辑 …

fiddler抓取,Android真机测试

1.配置Fiddler抓取并解密HTTPS包 Fiddler默认是不抓取HTTPS包的,需要进行相应的配置。 打开Fiddler,选择“Tools->Fiddler Options...” 2.在弹出的对话框中选择“HTTPS”选项卡: 3.勾选“Capture HTTPS CONNECTs”,接着勾选…

TCP/IP网络编程 第二十四章:制作HTTP服务器端

实现简单的Web服务器端 现在开始在HTTP协议的基础上编写Web服务器端。先给出Windows平台下的示例,再给出Linux下的示例。在这里我假设各位都有了有关HTTP的知识,如果不知道HTTP协议的具体内容可以参考的往期博客,有了这些基础就不难分析源代…

第三天 运维高级 MySQL主从复制

1.理解MySQL主从复制原理 1、master(binlog dump thread)主要负责Master库中有数据更新的时候,会按照binlog格式,将更新的事件类型写入到主库的binlog文件中。 2、I/O thread线程在Slave中创建,该线程用于请求Master&…

uniapp 微信小程序 Picker下拉列表数据回显问题

效果图&#xff1a; 1、template <template><view class"items select-box"><view class"items-text">品牌型号</view><picker change"bindBrandType" :value"brandIndex" :range"brandList"…

HTTP 什么样的响应才会被缓存

下面来讨论什么样的响应会被缓存&#xff0c;以及使用好已经缓存好的条件是什么。 缓存分为两步&#xff0c;首先将响应缓存下来&#xff0c;第二步将要发起一个请求的时候检查当前缓存是否可以使用缓存了的响应。 (1) 请求方法可以被缓存理解&#xff08;不只于 GET 方法&…

17 界面布局--登录界面

要点&#xff1a; 利用widgets做布局&#xff1a;水平&#xff0c;垂直&#xff0c;栅格 利用弹簧设置收缩 widget宽高比实际控件大很多&#xff1a;设置Fixed 如果需要去除其余边框间隙可以设置layout 将最小尺寸和最大尺寸设置为固定即为固定尺寸 设置窗口标题&#xff1a;wi…

SpringBoot3自动配置流程 SPI机制 核心注解 自定义starter

1. 自动配置流程 导入starter依赖导入autoconfigure寻找类路径下 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件启动&#xff0c;加载所有 自动配置类 xxxAutoConfiguration 给容器中配置功能组件组件参数绑定到 属性类中。xxxPrope…

前端技术Vue学习笔记--001

前端技术Vue学习笔记 文章目录 前端技术Vue学习笔记1、Vue2和Vue3比较2、Vue简介3、Vue快速上手4、插值表达式{{}}5、Vue响应式特性6、Vue指令6.1、v-html指令6.2、v-show指令和v-if指令6.3、v-else指令和v-else-if指令6.4、v-on指令6.4.1、v-on指令基础6.4.2、v-on调用传参 6.…

Kubernetes_核心组件_kubelet_kubelet服务全解析(二)

文章目录 前言kubelet 架构kubelet 职责Node管理(节点管理)Pod管理 kubelet管理Podkubelet如何管理当前节点上所有Podkubelet三个端口kubelet获取Pod清单kubelet通过CRI接口管理Pod以及里面的容器 PodWorker的工作细节PodWorker的工作细节PLEG组件PLEG报错 kubelet创建并启动Po…

arcgis-利用等高线数据生成dem栅格

1、打开cass&#xff0c;展高程点&#xff0c;绘制三角网&#xff0c;绘制等高线&#xff0c;删除三角网和高程点。如下&#xff1a; 2、得到的等高线图&#xff0c;如下&#xff1a; 3、保存文件为dwg格式&#xff0c;随后打开arcmap软件&#xff0c;打开dwg的线层数据&#x…

Linux yum 运行时提示编码问题错误

报错&#xff1a; UnicodeWarning: Unicode equal comparison failed to convert both arguments to Unicode - interpreting them as being unequal 解决&#xff1a; 这个报错是编码问题导致的 在文件头加上 # -*- coding:utf-8 -*- 对需要比较的中文字符变量如&#xff…

网络安全防御篇之安全问题及防火墙简介

网络安全常识及术语 网络的脆弱性 什么样的网络是安全的

单片机_按键——单击,双击,长按

#define KEY_DOWN_return 1 //单击 #define KEY_Double_return 2 //双击 #define KEY_Long_return 3 //长按 #define KEY_LONG_FREE_return 6 //长按结束#define double_time_MIN 80 //双击按键,在这个时间范围内,按下两次则认为有…

Java当中的深拷贝和浅拷贝

文章目录 一、前提二、浅拷贝1. BeanUtils实现浅拷贝 三、深拷贝1. 实现Cloneable接口并重写clone()方法&#xff1a;2. 使用序列化与反序列化&#xff1a; 一、前提 在计算机的内存中&#xff0c;每个对象都被赋予一个地址&#xff0c;该地址指向对象在内存中存储的位置。当我…

数据结构和算法解析:排序问题简易总结

直接插入排序 直接插入排序&#xff08;Straight Insertion Sorting&#xff09;的基本思想&#xff1a;在要排序的一组数中&#xff0c;假设前面(n-1) [n>2] 个数已经是排好顺序的&#xff0c;现在要把第n个数插到前面的有序数中&#xff0c;使得这n个数也是排好顺序的。如…

centos7部署websocket

django项目&#xff0c;中间使用websocket进行通讯&#xff0c;想部署到服务器上&#xff0c;按照之前部署项目的思路进行部署&#xff0c;但是失败了。解决了一下&#xff0c;在此记录。 主项目下有一子app&#xff0c;一模版文件&#xff0c;一静态文件。项目中主要用到dweb…

Factorization Machines(论文笔记)

样例一&#xff1a; 一个简单的例子,train是一个字典&#xff0c;先将train进行“one-hot” coding&#xff0c;然后输入相关特征向量&#xff0c;可以预测相关性。 from pyfm import pylibfm from sklearn.feature_extraction import DictVectorizer import numpy as np tra…

【MATLAB第59期】基于MATLAB的混沌退火粒子群CSAPSO-BP、SAPSO-BP、PSO-BP优化BP神经网络非线性函数拟合预测/回归预测对比

【MATLAB第59期】基于MATLAB的混沌退火粒子群CSAPSO-BP、SAPSO-BP、PSO-BP优化BP神经网络非线性函数拟合预测/回归预测对比 注意事项 不同版本matlab 不同电脑 加上数据集随机&#xff0c;BP权值阈值随机&#xff0c;进化算法种群随机&#xff0c;所以运行结果不一定和我运行…