golang range遍历的问题

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次。运行时结果如图所示:

2019-05-12_210632.png

  为什么会出现这种情况?于是我写了一些用例来测试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的一些使用特性。

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)
    }
}

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

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

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

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

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

编辑于

cassia

1 篇文章1 人订阅

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券