zoukankan      html  css  js  c++  java
  • Golang 之 我被 forrange 循环进去了

    看个例子

    在我们平时的代码场景中,常常需要改变切片中某个元素的值,先来看一下常见的代码实现方式:

    package main
    
    import "fmt"
    
    func test1() {
        slice1 := []int{1, 2, 3, 4}
        for _, val := range slice1 {
            val++
        }
      
        fmt.Println(slice1)
    }
    
    func test2() {
        slice2 := []int{1, 2, 3, 4}
        for k, _ := range slice2 {
            slice2[k]++
        }
      
        fmt.Println(slice2)
    }
    
    func test3() {
        slice3 := []int{1, 2, 3, 4}
        for i := 0; i < len(slice3); i++ {
            slice3[i]++
        }
        
        fmt.Println(slice3)
    }
    
    func main() {
        test1()
        test2()
        test3()
    }
    

    非常简单,test1() 中的修改并未对原数据产生影响,而 test2() 和 test3() 中的修改真正改变了原数据。我们看一下打印的结果:

    [1 2 3 4]
    [2 3 4 5]
    [2 3 4 5]
    

    最终输出也是跟我们预想的一致。

    主要原因是因为:

    • val是slice1内元素的副本,对val的改变不会导致slice1内元素的改变
    • 而在test2() 和 test3() 中是直接对切片进行索引修改,改变了底层的数组

    为什么会出现这种情况呢?我们去了解一下for - range 原理

    for - range 原理

    for range的实现

    // Arrange to do a loop appropriate for the type.  We will produce
    //   for INIT ; COND ; POST {
    //           ITER_INIT
    //           INDEX = INDEX_TEMP
    //           VALUE = VALUE_TEMP // If there is a value
    //           original statements
    //   }
    

    其中针对 slice 的编译方式如下:

    // The loop we generate:
    //   for_temp := range
    //   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
    //           original body
    //   }
    

    具体代码细节可查看 https://github.com/golang/gofrontend/blob/e387439bfd24d5e142874b8e68e7039f74c744d7/go/statements.cc#L5384 源代码

    从上面的源码我们可以看到,针对slice,for range 做了一下事情:

    • 对要遍历的 Slice 做一个拷贝
    • 获取长度大小
    • 使用常规for循环进行遍历,返回值的拷贝,并把值存放到全局变量 index 和 value中

    也就是说,对于 for k, val := range(slice) 环过程中,val 在循环内始终都是同一个全局变量

    结合上面的结论,我们接下来再看一道题:

    package main
    
    import "fmt"
    
    func test4() {
        s := []int{0, 1, 2, 3}
        m := make(map[int]*int)
    
        for index, value := range s {
            m[index] = &value
        }
        
        printMap(m)
    }
    
    func printtMap(m map[int]*int) {
        for key, value := range m {
            fmt.Printf("map[%v]=%v\n", key, *value)
        }
    }
    
    func main() {
        test4()
    }
    

    打印输出:

    map[2]=3
    map[3]=3
    map[0]=3
    map[1]=3
    

    test4() 中直接存的是地址,因为在整个for index, value := range s 循环中,value都是同一个全局变量,地址是一样的,每次遍历该地址上的值都会被新的值覆盖掉,所以在遍历结束后,该地址存的值是切片上的最后一个元素3

    如果我把 test4()方法 换成下面这种形式呢

    
    func test4() {
        s := []int{0, 1, 2, 3}
        m := make(map[int]*int)
    
        for index, value := range s {
            valueCopy := value
            m[index] = &valueCopy
        }
        
        printtMap(m)
    }
    

    上面主要改变是:每次进入循环体,都声明一个新变量valueCopy,并把value赋值给它,最后把新变量valueCopy的地址存到 m 中

    打印输出:

    map[0]=0
    map[1]=1
    map[2]=2
    map[3]=3
    

    原因是因为每次循环都声明新变量,对应的地址也是不一样的。

    我们再看一个闭包,其原理一样

    package main
    import (
        "fmt"
        "time"
    )
    func main()  {
        str := []string{"I","am","Echo 大叔"}
        for _, v := range str{
           // 每个goroutine的v的地址相同,都是为外部v的地址
            go func() {
               // 这里的v是引用外部变量v的地址
                fmt.Println(v)
            }()
        }
        
        time.Sleep(3 * time.Second)
    }
    

    实际上上面的代码会输出:

    Echo 大叔
    Echo 大叔
    Echo 大叔
    

    原因见注释

    上面闭包要想实现输出不同的值,可利用函数的值传递性质:

    package main
    import (
        "fmt"
        "time"
    )
    func main()  {
        str := []string{"I","am","Echo 大叔"}
        for _, v := range str{
           // 把外部的v值拷贝给函数内部的v
            go func(v string) {
                fmt.Println(v)
            }(v)
        }
        
        time.Sleep(3 * time.Second)
    }
    

    打印输出(打印顺序不一定一样):

    I
    am
    Echo 大叔
    

    对于slice

    由 for range 的原理我们可以知道 for i, v := range x,进入循环前会对x的长度进行快照,决定一共循环len(x)那么多次。后面x的变动不会改变循环次数。通过i,以及最新的x,把x[i]赋予给v。

    package main
    
    import (
        "fmt"
    )
    
    func main() {
        x := []int{1, 3, 5, 7, 9, 11, 13, 15}
        fmt.Println("start with ", x)
     
        for i, v := range x {
            fmt.Println("The current value is", v)
            x = append(x[:i], x[i+1:]...)
            fmt.Println("And after it is removed, we get", x)
        }
    }
    

    上面代码,我们在遍历切片的时候,每遍历一次就把该元素从切片中删掉

    打印输出:

    The current value is 1
    And after it is removed, we get [3 5 7 9 11 13 15]
    The current value is 5
    And after it is removed, we get [3 7 9 11 13 15]
    The current value is 9
    And after it is removed, we get [3 7 11 13 15]
    The current value is 13
    And after it is removed, we get [3 7 11 15]
    The current value is 15
    panic: runtime error: slice bounds out of range [5:4]
    
    goroutine 1 [running]:
    main.main()
            /data1/htdocs/go_project/src/github.com/cnyygj/go_practice/Interview/for_range.go:13 +0x398
    exit status 2
    

    从输出我们可以看出,for range 的循环次数取决于循环前会对遍历目标的长度进行快照,并不会随着遍历目标长度的修改而改变。所以最终会出现切片溢出的panic

    作业

    最后,留一道题给大家

    package main
    
    import (
       "fmt"
    )
    
    type Guest struct {
        id      int
        name    string
        surname string
        friends []int
    }
    
    func (self Guest) removeFriend(id int) {
        for i, other := range self.friends {
            if other == id {
                self.friends = append(self.friends[:i], self.friends[i+1:]...)
                break
            }
        }
    }
    
    func main() {
       test := Guest{0, "Echo", "大叔", []int{1,2, 3, 4, 5}}
       fmt.Println(test)
       test.removeFriend(4)
       fmt.Println(test) 
    }
    

    最终会打印输出:

    {0 Echo 大叔 [1 2 3 4 5]}
    {0 Echo 大叔 [1 2 3 5 5]}
    

    大家知道其中原因吗?欢迎评论交流~

    关注公众号 「大叔说码」,获取更多干货,下期见~

  • 相关阅读:
    hdoj:2075
    hdoj:2072
    hdoj:2071
    hdoj:2070
    hdoj:2069
    test001
    hdoj:2067
    hdoj:2061
    hdoj:2058
    hdoj:2057
  • 原文地址:https://www.cnblogs.com/dashu-saycode/p/14286234.html
Copyright © 2011-2022 走看看