一个range问题,求大神解解答下

直接上代码

var w []W

for i := 0 ; i < 5; i++{
        obj := W{
            K:1,
            V:int64(i*5),
        }
        fmt.Println(&(obj.V))
            //初始时地址是这样 
            //0xc042052088
            //0xc0420520c8
            //0xc0420520d8
            //0xc0420520e8
            //0xc0420520f8
        w = append(w,obj)

    }
    fmt.Println("=======================================")

    for _,v := range w{
        fmt.Println(&v.V)
        //这里结构体内的地址都指向一个地址
        //如果把var w []W 改成地址var w []*W就不会出现这个情况
        //0xc042052108
        //0xc042052108
        //0xc042052108
        //0xc042052108
        //0xc042052108
    }
    fmt.Println("=======================================")
    for i,_ :=range w{
        fmt.Println(&(w[i].V))
        //这样就不会出现地址相同的问题
        //0xc042086008
        //0xc042086018
        //0xc042086028
        //0xc042086038
        //0xc042086048
    }
已邀请:

gongxun - c/c++,golang,python,视频,深度学习领域

赞同来自: 嘿嘿嘿 HackerZ

因为v是分配在栈空间的变量, ,v:=range w是传值拷贝。 至于 i,:=range w为什么地址不同,自己去想。

yuanfanbin

赞同来自: 嘿嘿嘿

先回答下你这个提问(https://gocn.io/article/782),那个提问下没有格式。。。

var w []W
fmt.Printf("cap: %d, len: %d\n", cap(w), len(w))
for i := 0; i < 5; i++ {
obj := W{
K: 1,
V: 2,
}
fmt.Println(&(obj.V))
w = append(w, obj)
fmt.Printf("cap: %d, len: %d\n", cap(w), len(w))
}

fmt.Println("=======================================")
///这么用就不会出现这种情况
for i := range w {
fmt.Println(&(w[i].V))
}

在你给出的例子中,我们加上两句话 fmt.Printf("cap: %d, len: %d\n", cap(w), len(w)),然后执行

$ go run a.go
cap: 0, len: 0
0xc420084268
cap: 1, len: 1
0xc420084298
cap: 2, len: 2
0xc4200842b8
cap: 4, len: 3
0xc4200842d8
cap: 4, len: 4
0xc4200842f8
cap: 8, len: 5
=======================================
0xc420082088
0xc420082098
0xc4200820a8
0xc4200820b8
0xc4200820c8

w对象一开始并没有分配内存空间,随着你的appendw分片开始扩容,并填入你所给的数据,从输出可看出,你的append操作导致了w对象的cap增加,然而分片的cap的增加会导致什么现象,或着说内存会有什么变化,我们可以看源码是如何对分片append操作后是如何处理的,参考了go1.9.4源码 src/runtime/slice.gogrowslice 函数

// growslice handles slice growth during append.
// It is passed the slice element type, the old slice, and the desired new minimum capacity,
// and it returns a new slice with at least that capacity, with the old data
// copied into it.
// The new slice's length is set to the old slice's length,
// NOT to the new requested capacity.
// This is for codegen convenience. The old slice's length is used immediately
// to calculate where to write new values during an append.
// TODO: When the old backend is gone, reconsider this decision.
// The SSA backend might prefer the new length or to return only ptr/cap and save stack space.
func growslice(et *_type, old slice, cap int) slice {
... // 忽略细节
var p unsafe.Pointer
if et.kind&kindNoPointers != 0 {
... // 忽略细节
memmove(p, old.array, lenmem)
... // 忽略细节
} else {
... // 忽略细节
if !writeBarrier.enabled {
memmove(p, old.array, lenmem)
} else {
for i := uintptr(0); i < lenmem; i += et.size {
typedmemmove(et, add(p, i), add(old.array, i))
}
}
}

return slice{p, old.len, newcap}
}

我们忽略掉具体实现细节,可看出扩容时,golang底层使用了memmove函数将旧空间数据move到了新空间,此时地址必然发生变化。

DilonWu - https://github.com/AceDarkknight

赞同来自: 嘿嘿嘿

其实这个问题用 https://garbagecollected.org/2017/02/22/go-range-loop-internals/ 的就可以解答。首先代码中

for _,v := range w{
    fmt.Println(&v.V)
    fmt.Println(v.V)
}

的 v 在每次循环时都会重新分配内存,相当于

v:=w[i]

所以分配到同一个内存上是很正常的,而 v 的内容等于把 w[i] 的内容复制了一次。所以每次打印 v 的内容不一样。而

for i,_ :=range w{
fmt.Println(&(w[i].V))
}

就是每次取 w 底层数组 w[i] 内容的地址了,所以肯定不一样。

qiyin

赞同来自:

for range 时,实际是遍历一个浅拷贝的slice, 其v是一个临时变量, 这个临时变量是 slice 元素的拷贝, 在遍历时并不总是分配内存, 而是一直复用v的内存空间, 直接从内存拷贝数据到v的内存空间中的

推荐看 《golang for语句完全指南》

一下引用其中的一段介绍:

for语句的内部实现-array golang的for语句,对于不同的格式会被编译器编译成不同的形式,如果要弄明白需要看 golang的编译器和相关数据结构的源码, 数据结构源码还好,但是编译器是用C++写的,本人C++是个弱鸡,这里只讲array内部实现。

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

// 例如代码:  
array := [2]int{1,2}
for k,v := range array {
    f(k,v)
}

// 会被编译成:  
len_temp := len(array)
range_temp := array
for index_temp = 0; index_temp < len_temp; index_temp++ {
    value_temp = range_temp[index_temp]
    k = index_temp
    v = value_temp
    f(k,v)
}

所以像遍历一个数组,最后生成的代码很像C语言中的遍历,而且有两个临时变量index_temp,value_temp, 在整个遍历中一直复用这两个变量。所以会导致开头问题2的问题(详细解答会在后边)。

要回复问题请先登录注册