首页 > 其他分享 >(转)「Golang」for range 使用方法及避坑指南

(转)「Golang」for range 使用方法及避坑指南

时间:2023-02-06 18:12:43浏览次数:45  
标签:map 遍历 避坑 Golang range 循环 slice hv1

原文:https://blog.csdn.net/qq_37005831/article/details/114296008

前言
循环控制结构是一种在各种编程语言中常用的程序控制结构,其与顺序控制结构、选择控制结构组成了程序的控制结构,程序控制结构是指以某种顺序执行的一系列动作,用于解决某个问题。理论和实践证明,无论多复杂的算法均可通过顺序、选择、循环3种基本控制结构构造出来。

在Go中,提供了两种循环控制结构for和goto,但是后者不推荐使用(原因请查看艾兹格·迪杰斯特拉(Edsger Wybe Dijkstra)在1968年的一篇名称为《GOTO语句有害论》的论文),但是就作者而言goto在某些业务情况下,是很好用的,所以也不需要完全就反对他。

本文代码基于Go 1.16版本,不同版本如有差异请见谅

万能的for循环
在Go中,与c语言(及大部分语言)不同的是,去掉了while,do..while 循环,将其完全简化为for,虽然这样看起来是缺少了很多功能,但是while,do..while 等功能完全可以通过for来实现 。

接下来,通过几个代码来展示出,for循环,如何实现while,do..while 的相关逻辑。

func main() {
N := 10
for i := 0; i < N; i++ {
// TODO
}

for {
/*
* while true{
* // TODO
* }
*
*/
}

for N > 10 {
/*
* while N>10 {
* // TODO
* }
*
*/
}
for {
// TODO
// do{
// TODO
// }while(N>10)
if N <= 10 {
break
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
代码有些粗糙,望大家见谅,可以看得出来,一个for就可以完成while,do..while 所有的功能,从而看出Go的循环控制结构是多么的强大。

嫌for i := 0; i < N; i++ {}麻烦?来看看语法糖
在我们的Go编写的业务逻辑中,常用的循环方式,为经典的三段式循环,即for i := 0; i < N; i++ {},这种循环可以帮我们方便的遍历数组,切片等数据结构,还可以轻松的进行一定次数循环的操作,那么当我们想要遍历map和channel时,该如何呢?Go给我们提供了一个新关键字range来进行遍历,可以把它理解为一个三段式循环的语法糖,它不光可以遍历map和channel,同样的也可以遍历数组,切片等数据结构,但是与传统循环不同的是,他不可以进行普通的次数循环,那么接下来我们来看一下其遍历数组,切片,map和channel的相关操作以及所能碰到的坑。

遍历数组和切片
遍历数组和切片的方式都是一样的,因为切片的使用概率要大于数组,所以主要讲的切片的遍历,数组可以与其相同方式进行使用,首先 Show me code!

func main() {
slice := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// first
for range slice {
fmt.Println()
}
// second
for k := range slice {
fmt.Println(k)
}
// third
for k, v := range slice {
fmt.Println(k, v)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
接下来我们挨个的分析一下first,second,third三个注释下面的三种循环方式的区别以及一些特点:

使用first方式遍历数组和切片,因其range前没有接收变量,因此代表此次循环并不在意返回的索引以及数据,只关心循环次数。
second方式遍历数组和切片,因其range前仅有一个接收变量,因此代表此次循环仅关心返回的索引,不关心返回的数据,此代码等同于for k,_ := range slice
third方式遍历数组和切片,该方法是range的完全体使用形式,因此代表此次循环及关心返回的索引,也关心返回的数据。
三种形式,每种形式都有不同的编译编译器优化后的代码,因为range算是一个语法糖,最终其都会在编译期间优化为传统的三段式循环,那么接下来,来看一下这三种形式的编译期间被转换的代码。

下面的代码信息全部引用自:go/src/cmd/compile/internal/gc/range.go

优化后代码引自:https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-for-range/

首先提前说一下下面会出现的所有变量名的含义,该含义全部引用自编译器源代码。

v1,v2 =>代表 range前变量的索引、数据字段即

for k,v:=range slice{}中的k、v

ha => 被遍历元素的复制版

for k,v:=range slice {}中 slice的复制品

a => 可以理解为被遍历元素

for k,v:=range slice {}中 slice的复制品

即 ha 为 a 的复制版

hn => len(slice) ,即slice的长度

hv1 => 循环内的循环指针,可以理解为

for i := 0; i < N; i++ {}中的 i

忽略索引和数据
首先来看range前没有接收变量、此次循环并不在意返回的索引以及数据,只关心循环次数的循环方式,会产生什么样的译编译器优化后的代码:

ha := a
hv1 := 0
hn := len(ha)
v1 := hv1
for ; hv1 < hn; hv1++ {
...
}
1
2
3
4
5
6
7
由上述代码可以看出,在使用range循环的过程中,原数据被拷贝了一份,然后随后的三段式循环过程完全是围绕着这个复制版本进行操作。

忽略索引和数据就已经讲完了,没什么可说的,很简单,但需要注意的是,range循环被编译器优化改变后,是采用原数据的复制版本进行循环操作的,而不是直接使用原数据进行循环操作,因此,对于切片和数组来说,某些可能修改原数据但不会修改复制版本的操作,不会对每次循环的数据产生影响(还没进行测试,理论上是的)。

只关心索引
其次来看一下range前仅有一个接收变量,因此代表此次循环仅关心返回的索引,不关心返回的数据,会产生什么样的编译器优化后的代码:

ha := a
hv1 := 0
hn := len(ha)
v1 := hv1
for ; hv1 < hn; hv1++ {
v1 = hv1
...
}
1
2
3
4
5
6
7
8
由上述代码可以看出,使用了一个变量v1作为接受索引数据的变量,与其对应的是for k := range slice中的k,之所以没有使用hv1直接作为索引变量的原因,个人猜测是怕再循环过程中误修改循环指针的值,即hv1的值,而产生一些不明来源的问题。

从上面可以看出,range的每次循环都是针对一个变量进行循环的赋值,而不是每次循环重新申请内存空间,此处是一个很常见的出现面试题以及出现坑的地方,具体的内容一会会进行详解。

关心返回的索引和数据
最后来看一下range的完全体使用形式,此次循环及关心返回的索引,也关心返回的数据。因此其代码相对上一个也更复杂,接下来看一下会产生什么样的译编译器优化后的代码:

ha := a
hv1 := 0
hn := len(ha)
v1 := hv1
v2 := nil
for ; hv1 < hn; hv1++ {
tmp := ha[hv1]
v1, v2 = hv1, tmp
...
}
1
2
3
4
5
6
7
8
9
10
上述代码可见,v2是数据对应的变量,与其对应的是for k,v:= range slice中的v,v1则对应的是k,跟只关心索引的循环一样,每次循环都是针对一个变量进行循环的赋值,而不是每次循环重新申请内存空间。但是在循环中,出现的tmp变量是循环申请的,这是为什么?我个人理解是为了防止切片的元素类型是个指针,如果是个指针的话,直接赋值给v2,随后如果用户在循环过程中对其进行修改就会影响到原切片的复制版本的底层数据,这样是不好的行为(个人猜测,如果有更好的解释可以留言告知于我,在此表示感谢)。

遍历字符串
我们还可以使用range来遍历字符串,使用方式与数组和切片相同,但是有一个地方需要拿出来讲一下,例子代码如下:

func main() {
str:= "GopherEcho这是我的公众号"
for k, v := range str {
fmt.Print(k, ":", string(v), " ")
}
// out:
// 0:G 1:o 2:p 3:h 4:e 5:r 6:E 7:c 8:h 9:o 10:这 13:是 16:我 19:的 22:公 25:众 28:号
}
1
2
3
4
5
6
7
8
在k<10之前输出都很正常,为什么当k==10的时候每次间隔都为3了呢?这需要我们仍然从编译器优化后的代码的代码来看起,遍历字符串的时候,编译器依旧会将range修改为普通的三段式遍历形式,那么其被修改成什么了呢?接下来看代码:

下列代码摘自:go/src/cmd/compile/internal/gc/range.go 362-375行

ha := a
for hv1 := 0; hv1 < len(ha); {
hv1t := hv1
hv2 := rune(ha[hv1])
if hv2 < utf8.RuneSelf {
hv1++
} else {
hv2, hv1 = decoderune(ha, hv1)
}
v1, v2 = hv1t, hv2
// todo
}
1
2
3
4
5
6
7
8
9
10
11
12
接下来我来解释一下这段代码,首先仍然和遍历数组和切片一样,会对原对象进行一次拷贝,接下来开始使用传统三段式遍历方式进行遍历,接下来看第4行,其把当前遍历到的字节(字符串底层类似为byte数组)转换为rune类型,然后判断该rune类型是否为utf8码点,如果第5行判断为true,则代表当前的rune是一个ASCII字符,此时索引仅+1即可,如果为false,需要将其转换为对应长度的rune字符并且对应索引也会随之增加,decoderune(ha, hv1)这个函数,在go/src/runtime/utf8.go的第60行,感兴趣的可以去看看,在此就不做赘述了,随后,赋值给v1,v1即for k, v := range str的k和v。至此,字符串的遍历也讲完了,没啥可说的主要就是针对utf8码点进行了索引递增,每个中文字符占用3个byte长度。

decoderune函数的功能就是返回 s[k:]开头的非 ASCII 符文和 s 中符文后的索引。其中,s为传入的字符串,k为索引

遍历Map
在之前的文章里面,我进行了Map的源代码的解析,深入的解析了针对Map的读写以及扩容等源代码,接下来讲解一下通过使用range来遍历Map(说的像别的方式可以遍历一样

标签:map,遍历,避坑,Golang,range,循环,slice,hv1
From: https://www.cnblogs.com/liujiacai/p/17096320.html

相关文章

  • (转)golang-标准库(math/rand)
    原文:https://itpika.com/2019/07/09/go/library-math-rand/rand包是go提供用来产生各种各样随机数的包,本文对这些产生随机数的方法做一下介绍。注意:rand生成的数值虽然说......
  • Docker+vulhub靶机搭建避坑指南
    Docker+vulhub安装避坑指南1.Docker安装kali需提前换源安装好pipsudoaptinstalldocker.iopipinstalldocker-compose2.搭建vulhub直接使用镜像:gitclonehtt......
  • (转)深入golang -- select
    原文:https://zhuanlan.zhihu.com/p/509148906老规矩相信大家已经知道select应用的特性,这里主要是介绍select的底层原理。select底层原理主要分为两部:select语句......
  • (转)golang学习之--select--case 原理
    原文:https://blog.csdn.net/cyb_17302190874/article/details/108244683Go的select语句是一种仅能用于channl发送和接收消息的专用语句,此语句运行期间是阻塞的;当select中......
  • golang的闭包
    funcAddUpper()myFun{//闭包:返回的函数与这个函数引用到函数外的变量n形成一个整体,共同生成闭包,反复调用f1函数即AddUpper函数,n只初始化一次,所以返回的值的累加的......
  • Hive窗口函数中range和rows的区别
    说明聊到hive,就少不了灵活的开窗函数,今天介绍下开窗函数中over子句内部经常会用到的rows和range的用法;数据准备createtabletemp_id_0116(idint)stor......
  • (转)golang常用库之-标准库 sync包| go语言如何实现单例模式、读写锁(sync.RWMutex)
    原文:https://blog.csdn.net/inthat/article/details/124218961golang常用库之-标准库sync包Golangsync包提供了基础的异步操作方法,包括互斥锁Mutex,执行一次Once和并发等......
  • (转)深入浅出Golang Runtime
    原文:https://www.cnblogs.com/lovezbs/p/14467801.html本文为腾讯NOW直播研发工程师郝以奋在8月19日深圳GopherMeetup上的分享,以下为根据PPT进行的详细注解。介绍......
  • (转)Golang标准库——runtime
    原文:https://www.jianshu.com/p/c1b6de70c004runtimeruntime包提供和go运行时环境的互操作,如控制go程的函数。它也包括用于reflect包的低层次类型信息;参见》reflect报......
  • golang变量
    1.golang变量命名规则由26个英文字母大小写,0-9,_组成。变量名不能以数字开头。变量都是先声明再使用,一旦声明必须使用。2.golang变量赋值方法2.1单个变量赋值......