大家好,我是peachestao,今天是国庆节的最后一天,大部分人应该都已经返程了,不知道大家这个国庆玩的怎么样。

前段时间工作有点忙,加上生活中的一些事导致一个月没更新了,以后会加快更新频率。

分享是一个再学习的过程,不知道大家有没有过这样的体会:某个知识点写之前觉得掌握透了,但是当你写出来的时候发现自己对知识点的理解有盲区,只掌握了个大概,不知道来龙去脉,无法自圆其说。

秉着”写出来并让大家理解就是自己完全掌握透了的“分享原则,会逼着自己查阅各种资料并亲手实践,在这个过程中你会恍然大悟:”原来如此“。

分享前的我觉得懂->分享时的疑惑->查阅资料->亲手实践->新的认知,这个过程就是再学习的过程,也是精进的过程。

话不多说,进入今天的分享主题,for range语句是业务开发中编写频率很高的代码,其中会有一些常见的坑,看完这篇文章会让你少入坑。

先看一下提纲:

  • for range的基本用法

  • for range和for的区别

  • for range容易踩的坑

  • for range和for性能比较

  • for range的底层原理

  • 总结

for range基本用法

range是Golang提供的一种迭代遍历手段,可操作的类型有数组、切片、string、map、channel等

1、遍历数组

myArray := [3]int{1, 2, 3}
for i, ele := range myArray {fmt.Printf("index:%d,element:%d\n", i, ele)fmt.Printf("index:%d,element:%d\n", i, myArray[i])
}

直接取元素或通过下标取

2、遍历slice

mySlice := []string{"I", "am", "peachesTao"}
for i, ele := range mySlice {fmt.Printf("index:%d,element:%s\n", i, ele)fmt.Printf("index:%d,element:%s\n", i, mySlice[i])
}

直接取元素或通过下标取

3、遍历string

s:="peachesTao"
for i,item := range s {fmt.Println(string(item))fmt.Printf("index:%d,element:%s\n", i, string(s[i]))
}

直接取元素或通过下标取

注意:循环体中string中的元素实际上是byte类型,需要转换为字面字符

4、遍历map

myMap := map[int]string{1:"语文",2:"数学",3:"英语"}
for key,value := range myMap {fmt.Printf("key:%d,value:%s\n", key, value)fmt.Printf("key:%d,value:%s\n", key, myMap[key])
}

直接取元素或通过下标取

5、遍历channel

myChannel := make(chan int)
go func() {for i:=0;i<10;i++{time.Sleep(time.Second)myChannel <- i}
}()go func() {for c := range myChannel {fmt.Printf("value:%d\n", c)}
}()

channel遍历是循环从channel中读取数据,如果channel中没有数据,则会阻塞等待,如果channel已被关闭,则会退出循环。

for range 和 for的区别

  • for range可以直接访问目标对象中的元素,而for必须通过下标访问

  • for frange可以访问map、channel对象,而for不可以

for range容易踩的坑

下面的例子是将mySlice中每个元素的后面都加上字符"-new"

mySlice := []string{"I", "am", "peachesTao"}
for _, ele := range mySlice {ele=ele+"-new"
}
fmt.Println(mySlice)

结果:

[I am peachesTao]

打印mySlice发现元素并没有更新,为什么会这样?

原因是for range语句会将目标对象中的元素copy一份值的副本,修改副本显然不能对原元素产生影响

为了证明上述结论,在遍历前和遍历中打印出元素的内存地址

mySlice := []string{"I", "am", "peachesTao"}
fmt.Printf("遍历前首元素内存地址:%p\n",&mySlice[0])
for _, ele := range mySlice {ele=ele+"-new"fmt.Printf("遍历中元素内存地址:%p\n",&ele)
}
fmt.Println(mySlice)

结果:

遍历前第一个元素内存地址:0xc000054180
遍历前第二个元素内存地址:0xc000054190
遍历前第三个元素内存地址:0xc0000541a0
遍历中元素内存地址:0xc000010200
遍历中元素内存地址:0xc000010200
遍历中元素内存地址:0xc000010200
[I am peachesTao]

可以得出两个结论:

  • 遍历体中的元素内存地址已经发生了变化,生成了元素副本,至于产生副本的原因在“for range底层原理”段落中会有介绍

  • 遍历体中的只生成了一个全局的元素副本变量,不是每个元素都会生成一个副本,这个特点也值得大家注意,否则会踩坑。

比如遍历mySlice元素生成一个[]*string类型的mySliceNew,要通过一个中间变量取中间变量的地址(或者通过下标的形式访问元素也可以)加入mySliceNew,如果直接取元素副本的地址会导致mySliceNew中所有元素都是一样的,如下:

mySlice := []string{"I", "am", "peachesTao"}
var mySliceNew []*string
for _, item := range mySlice {itemTemp := itemmySliceNew = append(mySliceNew, &itemTemp)//mySliceNew = append(mySliceNew, &item) 错误的做法
}

回到刚才那个问题,如何能在遍历中修改元素呢?答案是直接通过下标访问slice中的元素对其赋值,如下:

mySlice := []string{"I", "am", "peachesTao"}
for i, _ := range mySlice {mySlice[i] = mySlice[i]+"-new"
}
fmt.Println(mySlice)

结果:

[I-new am-new peachesTao-new]

可以看到元素已经被修改

for range和for性能比较

我们定义一个结构体Item,包含int类型的id字段,对结构体数组分别使用for、for range item、for range index的方式进行遍历,下面是测试代码(直接引用“Go语言高性能编程”这篇文章中的例子,下面的reference中有链接地址)

type Item struct {id  int
}func BenchmarkForStruct(b *testing.B) {var items [1024]Itemfor i := 0; i < b.N; i++ {length := len(items)var tmp intfor k := 0; k < length; k++ {tmp = items[k].id}_ = tmp}
}func BenchmarkRangeIndexStruct(b *testing.B) {var items [1024]Itemfor i := 0; i < b.N; i++ {var tmp intfor k := range items {tmp = items[k].id}_ = tmp}
}func BenchmarkRangeStruct(b *testing.B) {var items [1024]Itemfor i := 0; i < b.N; i++ {var tmp intfor _, item := range items {tmp = item.id}_ = tmp}
}

运行基准测试命令:

go test -bench . test/for_range_performance_test.go

测试结果:

goos: darwin
goarch: amd64
BenchmarkForStruct-4             3176875               375 ns/op
BenchmarkRangeIndexStruct-4      3254553               369 ns/op
BenchmarkRangeStruct-4           3131196               384 ns/op
PASS
ok      command-line-arguments  4.775s

可以看出:

for range 通过Index和直接访问元素的方式和for的方式遍历性能几乎无差异

下面我们在Item结构体添加一个byte类型长度为4096的数组字段val

type Item struct {id  intval [4096]byte
}

再运行一遍基准测试,结果如下:

goos: darwin
goarch: amd64
BenchmarkForStruct-4             2901506               393 ns/op
BenchmarkRangeIndexStruct-4      3160203               381 ns/op
BenchmarkRangeStruct-4              1088            948678 ns/op
PASS
ok      command-line-arguments  4.317s

可以看出:

  • for range通过下标遍历元素的性能跟for相差不大

  • for range直接遍历元素的性能比for慢近1000倍

结论:

  • for range通过下标遍历元素的性能跟for相差不大

  • for range直接遍历元素的性能在元素为小对象的情况下跟for相差不大,在元素为大对象的情况下比for慢很多

for range的底层原理

对于for-range语句的实现,可以从编译器源码中找到答案。
编译器源码gofrontend/go/statements.cc/For_range_statement::do_lower()【链接见下方reference方法中有如下注释。

// Arrange to do a loop appropriate for the type.  We will produce
//   for INIT ; COND ; POST {
//           ITER_INIT
//           INDEX = INDEX_TEMP
//           VALUE = VALUE_TEMP // If there is a value
//           original statements
//   }

可见range实际上是一个C风格的循环结构。range支持string、数组、数组指针、切片、map和channel类型,对于不同类型有些细节上的差异。

1、range for slice

下面的注释解释了遍历slice的过程:

For_range_statement::lower_range_slice

// The loop we generate:
//   for_temp := range
//   len_temp := len(for_temp)
//   for index_temp = 0; index_temp < len_temp; index_temp++ {
//           value_temp = for_temp[index_temp]
//           index = index_temp
//           value = value_temp
//           original body
//   }

遍历slice前会先获得slice的长度len_temp作为循环次数,循环体中,每次循环会先获取元素值,如果for-range中接收index和value的话,则会对index和value进行一次赋值,这就解释了对大元素进行遍历会影响性能,因为大对象赋值会产生gc

由于循环开始前循环次数就已经确定了,所以循环过程中新添加的元素是没办法遍历到的。

另外,数组与数组指针的遍历过程与slice基本一致,不再赘述。

2、range for map 

下面的注释解释了遍历map的过程:

For_range_statement::lower_range_map

// The loop we generate:
//   var hiter map_iteration_struct
//   for mapiterinit(type, range, &hiter); hiter.key != nil; mapiternext(&hiter) {
//           index_temp = *hiter.key
//           value_temp = *hiter.val
//           index = index_temp
//           value = value_temp
//           original body
//   }

遍历map时没有指定循环次数,循环体与遍历slice类似。由于map底层实现与slice不同,map底层使用hash表实现,插入数据位置是随机的,所以遍历过程中新插入的数据不能保证遍历到。

3、range for channel

遍历channel是最特殊的,这是由channel的实现机制决定的:

For_range_statement::lower_range_channel

// The loop we generate:
//   for {
//           index_temp, ok_temp = <-range
//           if !ok_temp {
//                   break
//           }
//           index = index_temp
//           original body
//   }

一直循环读数据,如果有数据则取出,如果没有则阻塞,如果channel被关闭则退出循环

注:

  • 上述注释中index_temp实际上描述是有误的,应该为value_temp,因为index对于channel是没有意义的。

总结

  • 使用index,value接收range返回值会产生一次数据拷贝,视情况考虑不接收,以提高性能

  • for-range的实现实际上是C风格的for循环

参考资料

【《Go专家编程》Go range实现原理及性能优化剖析 https://my.oschina.net/renhc/blog/2396058

【面试官:用过go中的for-range吗?这几个问题你能解释一下原因吗?】https://zhuanlan.zhihu.com/p/217987219

【Go语言高性能编程】https://geektutu.com/post/hpg-range.html

【gofrontend】https://github.com/golang/gofrontend/blob/master/go/statements.cc

go语言中的for range相关推荐

  1. linux strcpy 用法,由Linux中管道的buffer,浅谈C语言中char类型字符串拷贝使用strcpy()和=赋值符号的区别...

    今天在写父子进程用两个单向管道通信时,出现了错误: Segmentation fault (core dumped) 打开core文件发现: 附上源码: 1 #include 2 #include 3 ...

  2. Java 语言中 Enum 类型的使用介绍

    Enum 类型的介绍 枚举类型(Enumerated Type) 很早就出现在编程语言中,它被用来将一组类似的值包含到一种类型当中.而这种枚举类型的名称则会被定义成独一无二的类型描述符,在这一点上和常 ...

  3. go语言中channel的创建和销毁以及匿名函数的使用

    channel的创建 go语言中,任意类型前面加上关键字chan即可声明对应类型的通道,创建通道需要使用make,make也用于map 和slice的创建 创建一个通道 /*刚创建的通道是nil*/ ...

  4. sql 语言中 when case 用法

    sql语言中有没有相似C语言中的switch case的语句?? 没有,用case when 来取代就行了. 比如,以下的语句显示中文年月 select getdate() as 日期,case mo ...

  5. sql 语言中 when case 用法

    sql语言中有没有相似C语言中的switch case的语句?? 没有,用case   when   来取代就行了.              比如,以下的语句显示中文年月           sel ...

  6. Go语言编程—Go语言中JSON的处理(map、struct 和 JSON字符串的相互转换)

    JSON的简单介绍 JSON (JavaScript Object Notation)是一种比XML更轻量级的数据交换格式,在易于人们阅读和编写的同时,也易于程序解析和生成.尽管JSON是JavaSc ...

  7. Java中的enum详细解析------Java 语言中 Enum 类型的使用介绍

    Enum 类型的介绍 枚举类型(Enumerated Type) 很早就出现在编程语言中,它被用来将一组类似的值包含到一种类型当中.而这种枚举类型的名称则会被定义成独一无二的类型描述符,在这一点上和常 ...

  8. c语言中 函数值类型的定义可以,C语言中,函数值类型的定义可以缺省,此时函数值的隐含类型是...

    C语言中,函数值类型的定义可以缺省,此时函数值的隐含类型是 更多相关问题 An allophone refers to any of the different forms of a ______. ...

  9. go语言中error的分类与用法

    go语言中error的分类与用法 原文引用:极客时间中的课程<Go error处理最佳实践> 前言:本文要讨论的就是go中error的基本原理/类型,以及最重要的几个问题: go代码开发中 ...

最新文章

  1. springmvc常用注解标签详解
  2. 环形链表找入口,真的太妙了
  3. 潘建伟团队再次展示量子计算优越性!“祖冲之号”1.2小时就能完成超算8年计算量...
  4. windowsphone开发_[app开发定制公司]开发app需要什么技术呢?
  5. PowerPoint动画制作时的需要注意的N个事项
  6. 真刑啊!蔚来员工用公司服务器挖矿,已供认不讳
  7. ALV的SAP自带标准程序实例
  8. 我在中国图书网不愉快的购书经历!!!!!!!
  9. git reset 命令详解(一)—— Git 学习笔记 07
  10. linux-basic(9)文件与文件系统的压缩与打包
  11. 支持向量机-SVM-最优化公式推导记录
  12. 当WEB2.0从概念变成电子商务网站的工具
  13. C#毕业设计——基于C#+asp.net+SQL server的客户关系管理系统设计与实现(毕业论文+程序源码)——客户关系管理系统
  14. 数学传奇3——神话的破灭
  15. python pip install fitter 失败解决方案
  16. Python快速入门 满满都是干货!
  17. matlab 医学断层图像,利用MATLAB实现CT断层图像的三维重建
  18. docker学习——杂记
  19. 基于SSM的新闻管理系统的设计与实现 毕业论文+项目源码及数据库文件、
  20. 简单易用的运动控制卡(十一):运动的暂停恢复和速度倍率设置

热门文章

  1. Python实用案例:一秒自动生成工资条。
  2. 职教计算机应用基础,中等职业学校计算机应用基础教学大纲
  3. C/C++餐厅自动化点餐系统
  4. 社保可以这么报销?现在知道还不晚!
  5. finnal finnally finalized
  6. Python 通过创建MyMath类计算圆的周长面积球的表面积体积
  7. 打印机配置,关于打印时候显示“错误-正在打印”
  8. 中高级测试工程师基础知识必备之selenium篇
  9. 管理学经典定律汇粹及解析一览
  10. 5. 对称symmetries