关于range的一个知识点

func main() {
    v := []int{1, 2, 3}
    for i := range v {
        v = append(v, i)
    }
    fmt.Println(v)
}

请问这段这段代码的执行结果是什么?为什么会这个结果? 欢迎大家讨论

已邀请:

DilonWu - https://github.com/AceDarkknight

赞同来自: yulibaozi like2019 krew lmw 皇虫 Mrwxj Hawken shockerli更多 »

输出是[1 2 3 0 1 2],这是因为 range 会把 v 的内容复制一次,也就是创建了一个cap,len和v相等,指向v底层数组的 slice struct,所以 v = append(v, i) 相当于在v后面添加 index。经过编译后这段代码相当于

for_temp := v
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
        v = append(v, index)
}

更加详细的解析可以参考这篇文章:https://garbagecollected.org/2017/02/22/go-range-loop-internals/

simple - 既要有梦想,又要有实力

赞同来自: yulibaozi DilonWu

其实我觉得这个问题的关键点是:slice, map和chan的底层数据结构到底是怎样的?在赋值操作(包括range, 函数调用入参,函数返回值等)的过程中,这三个数据结构到底copy了什么?因为这三个一般都用builtin make函数初始化,有许多人都把它们简单的理解为"引用类型"。但其实没那么简单。 以下是我的理解:

  1. slice在runtime/slice.go的makeslice返回的是slice类型,而slice本质上是一个结构体
    type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
    }

    在赋值操作过程中,其实传递的结构体本身(unsafe.Sizeof值始终为24),所以你在做类似如下操作

    s1 := []int{1, 2, 3}
    s2 := s1

    对其中一方的len或cap字段做修改并不影响另一方,因为两者只有array字段指向同一块内存区域。回到楼主问题,range v赋值了v, 然后对原来的v的修改,并不影响range v的len, cap字段,所以range v可以正常结束。 类似的操作如:

    s := []int{1, 2, 3}
    call_func(s) {
    //do something for s
    }

    在将s传递给call_func函数之后,在对s的任何操作,对函数调用中的copy s并“不可见”(即使改了array字段,因为我们的大部分操作都是基于len和cap字段,函数中的copy s感知不到)。相反在函数中对copy s的操作,对外部的s也将"不可见"。

  2. map和chan在runtime/hashmap.go, chan.go的makemap, makechan函数中返回的是hmap, hchan指针(unsafe.Sizeof的值始终为8, 64bit主机上),可以认为这才是真正的"引用类型",所以可以认为在map和chan变量之间赋值传递的是指针或引用。因此:
    m1 := map[string]string{}
    m2 := m1

    不管是通过m1或m2对map进行操作,对方都可见。

h12 - https://h12.io/about

赞同来自: yulibaozi

The range expression x is evaluated once before beginning the loop, with one exception: if at most one iteration variable is present and len(x) is constant, the range expression is not evaluated.

https://golang.org/ref/spec#For_statements

mileStone - 90后it

赞同来自: alphayan

猛的一看,以为什么罕见的坑呢, 谁知:运行下面代码,应该可以解释你的问题了:

v := []int{1, 2, 3} for i := range v { v = append(v, i) fmt.Println(v, i)

}
fmt.Println(v)

v1 := []int{1, 2, 3}
for _, i := range v1 {
    v1 = append(v1, i)
    fmt.Println(v1, i)

}
fmt.Println(v1)

koala

赞同来自:

1 2 3 0 1 2 有问题吗?

momaek - Hello World

赞同来自:

如果把 slice 换成 map 又会有不一样的体验

like2019

赞同来自:

@DilonWu 为什么经过编译后的代码不是这样子呢?

for_temp := v

len_temp := len(for_temp)

for index_temp = 0; index_temp < len_temp; index_temp++ {

    ndex = index_temp
    v = append(v, index)

}

zengming00 - 野生程序猿

赞同来自:

好家伙,这种问题平时写程序根本不能这么写,但是面试题就喜欢这种 我这还有一个关于map的,go语言在range map时删除key,安全吗? https://blog.csdn.net/zengming00/article/details/79004402 即使可以这么写,仍然不建议这样写,非常不建议,因为这不直观,你现在知道是安全的,但过段时间你又忘了这到底行不行,于是又去找资料,不符合DRY

alphayan

赞同来自:

按照我的理解,range返回有2个值的时候,可以省略第一或者第二个值

for k,v:=range X{}//不省略
for k:=range X{}//省略第二个值
for _,v:=range X{}//省略第一个值

这个样子来看就很好理解了,拼接的其实是切片索引,索引从0开始。

要回复问题请先登录注册