golang的range特性,给我们对array、slice、string、map等结构进行取值时,提供了简洁快速的遍历方法。但是在使用时,要注意值拷贝和指针拷贝的区别。最近在项目中,就发现了一个因为range使用不当引起的bug。
项目代码如下所示:
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次。运行时结果如图所示:
为什么会出现这种情况?于是我写了一些用例来测试range的性质:
1.遍历array
func main() {
arr := [5]int{1, 2, 3, 4, 5}
for _, v := range arr {
fmt.Println(v)
}
}
这是range的基本用法,结果也不出意料:
1
2
3
4
5
接着,我试着打印v的地址:
2.遍历array输出地址
func main() {
arr := [5]int{1, 2, 3, 4, 5}
for _, v := range arr {
fmt.Println(&v)
}
}
这里发现问题了,5次输出的居然是同一个地址:
0xc000054080
0xc000054080
0xc000054080
0xc000054080
0xc000054080
为了排除是array存储指针类型的原因,我申明了一个指针数组进行遍历:
3.遍历指针array
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)
}
}
可见输出是没有问题的。
0xc00000e2a0
0xc00000e2a8
0xc00000e2b0
0xc00000e2b8
0xc00000e2c0
可以想到,项目中的bug是因为rs.middlewares = append(rs.middlewares, &m)
代码中,&m
的值始终固定,所以添加的永远是最后一个元素:trace。为了避免这个问题,我将middlewares数组改为实例类型:middlewares []mux.MiddlewareFunc
,问题果然解决了。但是在项目逻辑中,每一次http请求都会调用Swap
方法,而且middlewares初始化之后就不会变。因此,采用实例类型会增加不必要的实例创建。为此,我查询了资料,了解了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来进行取值:
func main() {
arr := [5]int{1, 2, 3, 4, 5}
for i, _ := range arr {
fmt.Println(&arr[i])
}
}
这样输出结果就正确了:
0xc00000e2a0
0xc00000e2a8
0xc00000e2b0
0xc00000e2b8
0xc00000e2c0
或者还有一个方法,就是每次创建一个变量,然后输出该变量的地址:
func main() {
arr := [5]int{1, 2, 3, 4, 5}
for _, v := range arr {
a := v
fmt.Println(&a)
}
}
这也不失为一个解决方法.但是,这个方法有几个问题。
因此,正确的做法应该是采用第一种方法。并且在我们不需要使用局部变量时,仅仅使用for i, _ := range arr
或更简洁的写法for i := range arr
来替代for i,v := range arr
,避免局部变量的创建。当然,不使用v
的话会因为unused variable编译错误。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有