前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >golang range遍历的问题

golang range遍历的问题

原创
作者头像
inuyasha
修改2019-05-12 22:26:58
2.3K0
修改2019-05-12 22:26:58
举报
文章被收录于专栏:cassiacassia

golang的range特性,给我们对array、slice、string、map等结构进行取值时,提供了简洁快速的遍历方法。但是在使用时,要注意值拷贝和指针拷贝的区别。最近在项目中,就发现了一个因为range使用不当引起的bug。


项目代码如下所示:

代码语言:txt
复制
func (rs *RouterSwapper) Use(mwf ...mux.MiddlewareFunc) {
	for _, m := range mwf {
		rs.middlewares = append(rs.middlewares, &m)
	}
}

func (rs *RouterSwapper) Swap(newRouter *mux.Router) {
	for _, midleware := range rs.middlewares {
		newRouter.Use(*midleware)
	}
    ...
}

  RouterSwapper是一个路由器的包装,在Use方法中,我通过将加载的中间件存储到middlewares的数组中,然后在Swap方法中,给新的Router循环添加*middlewares中的元素。其中,*mux.MiddlewareFunc为一些自定义方法的引用:type MiddlewareFunc func(http.Handler) http.Handler。但是在实际运行中,我发现middlewares中始终只有一个元素,并且重复添加出现了3次。运行时结果如图所示:

2019-05-12_210632.png
2019-05-12_210632.png

  为什么会出现这种情况?于是我写了一些用例来测试range的性质:


1.遍历array

代码语言:txt
复制
func main() {
    arr := [5]int{1, 2, 3, 4, 5}
    for _, v := range arr {
        fmt.Println(v)
    }
}

这是range的基本用法,结果也不出意料:

代码语言:txt
复制
1
2
3
4
5

接着,我试着打印v的地址:

2.遍历array输出地址

代码语言:txt
复制
func main() {
    arr := [5]int{1, 2, 3, 4, 5}
    for _, v := range arr {
        fmt.Println(&v)
    }
}

这里发现问题了,5次输出的居然是同一个地址:

代码语言:txt
复制
0xc000054080
0xc000054080
0xc000054080
0xc000054080
0xc000054080

为了排除是array存储指针类型的原因,我申明了一个指针数组进行遍历:

3.遍历指针array

代码语言:txt
复制
func main() {
    arr := [5]int{1, 2, 3, 4, 5}
    arr2 := [5]*int{&arr[0],&arr[1],&arr[2],&arr[3],&arr[4]}
    for _, v := range arr2 {
        fmt.Println(v)
    }
}

可见输出是没有问题的。

代码语言:txt
复制
0xc00000e2a0
0xc00000e2a8
0xc00000e2b0
0xc00000e2b8
0xc00000e2c0

结论

  可以想到,项目中的bug是因为rs.middlewares = append(rs.middlewares, &m)代码中,&m的值始终固定,所以添加的永远是最后一个元素:trace。为了避免这个问题,我将middlewares数组改为实例类型:middlewares []mux.MiddlewareFunc,问题果然解决了。但是在项目逻辑中,每一次http请求都会调用Swap方法,而且middlewares初始化之后就不会变。因此,采用实例类型会增加不必要的实例创建。为此,我查询了资料,了解了range的一些使用特性。

range特性

  go官方文档中对range进行了详细的解释:

Range expression

1st value

2nd value

array or slice a : nE, *nE, or []E

index i int

ai E

string s : string type

index i int

see below rune

map m : mapKV

key k K

mk V

channel c : chan E, <-chan E

element e E  

  range可以接受4中类型,在下文中提到了在range中使用:=符号赋值的情况:

The iteration variables may be declared by the "range" clause using a form of short variable declaration (:=). In this case their types are set to the types of the respective iteration values and their scope is the block of the "for" statement; they are re-used in each iteration. If the iteration variables are declared outside the "for" statement, after execution their values will be those of the last iteration.

  即在使用:=声明迭代变量时,其类型会设置为相应迭代类型,作用域在for范围内。该变量在迭代中会重复使用,如果变量声明在for外,那么迭代结束后变量值为最后一次赋值。

  读到这里,我们已经知道了实例2中,输出地址始终为0xc000054080的原因。因为变量v被重复使用,而它的地址不会变更。

解决方案

  为了解决这个问题,我们可以利用index来进行取值:

代码语言:txt
复制
func main() {
    arr := [5]int{1, 2, 3, 4, 5}
    for i, _ := range arr {
        fmt.Println(&arr[i])
    }
}

这样输出结果就正确了:

代码语言:txt
复制
0xc00000e2a0
0xc00000e2a8
0xc00000e2b0
0xc00000e2b8
0xc00000e2c0

或者还有一个方法,就是每次创建一个变量,然后输出该变量的地址:

代码语言:txt
复制
func main() {
    arr := [5]int{1, 2, 3, 4, 5}
    for _, v := range arr {
        a := v
        fmt.Println(&a)
    }
}

这也不失为一个解决方法.但是,这个方法有几个问题。

  • 对于每一次迭代,都会创建一个新的实例。int数组还好,像项目中使用到的middleware实例,有的就不小了。频繁创建实例或许会对运行速度以及gc造成影响。
  • 代码不简洁,并且抛弃了指针原有在赋值传递过程中的便捷性。
  • 造成了局部变量的逃逸。如果是将变量a添加到其他外边引用中,那么变量a便逃逸出方法的使用范围。虽然对于go来说,gc的存在保证我们不需要关心内存的销毁和分配。

  因此,正确的做法应该是采用第一种方法。并且在我们不需要使用局部变量时,仅仅使用for i, _ := range arr或更简洁的写法for i := range arr来替代for i,v := range arr,避免局部变量的创建。当然,不使用v的话会因为unused variable编译错误。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 结论
  • range特性
  • 解决方案
相关产品与服务
消息队列 TDMQ
消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档