暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

go语言系列5 - 你不得不知道slice的那些事

栋总侃技术 2021-04-15
312

我们直接撸代码来了解slice的实现细节,首先看下以下代码示例:

    package main

    import "fmt"

    func main() {
    s := make([]int, 5, 10)
    for i := 0; i < 5; i++ {
    s[i] = i
    }
    change(s)
    fmt.Println("s: ", s)
    }

    func change(s1 []int) {
      s1[0] = -1
      fmt.Println("s1: ", s1)
    }
    复制

    大家思考下s,s1分别输出什么呢?这个答案应该比较简单,输出的结果为:

      s2:  [-1 1 2 3 4]
      s: [-1 1 2 3 4]
      复制

      我们再接着看下面这个代码示例:

        package main

        import "fmt"

        func main() {
        s := make([]int, 5, 10)
        for i := 0; i < 5; i++ {
        s[i] = i
        }
        ap(s)
        fmt.Println("s: ", s)
        }

        func ap(s2 []int) {
          s1 = append(s2, 100)
          fmt.Println("s2: ", s2)
        }
        复制

        大家可以先不着急往下看,先思考下s2会输出什么?s又会输出什么呢?

        避免大家看到答案,我在中间多留白一些空间

        答案是:

          s1:  [0 1 2 3 4 100]
          s: [0 1 2 3 4]
          复制

          很快答对的读者估计对slice的底层实现比较理解,经过一番思考得出答案的读者或许有一部分是蒙对的,而答错的同学需要把该文章下列内容认真地读完了。

          为什么会产生上述两个例子的输出结果呢?Go的传参到底是值传递还是引用传递呢?为什么案例1改变了原slice而案例2没有任何改变?我们首先看下slice的结构:

            // runtime/slice.go
            type slice struct {
            array unsafe.Pointer
            len int
            cap int
            }
            复制

            结构中包含

            • array:指向一片内存空间指针,用以存储元素;

            • len:数组存放的元素个数;

            • cap:数组的容量,大于等于len。

            在这里,我们可以肯定的是Go中函数传参数使用的是值传递

            我们再回过头来看下案例1:

              func change(s1 []int) {
              s1[0] = -1
              fmt.Println("s1: ", s1)
              }
              复制

              可以理解为s1是从s的拷贝出来的一个对象,s、s1两个对象的len、cap相等,array指向同一片内存空间。当对s1[0]的值进行改变时,改变的内存空间与s [0]是同一片内存空间,所以s[0]相应的也发生了改变。

              在案例2中,同样的s1是从s的拷贝出来的一个对象,s、s1两个对象的len、cap相等,array指向同一片内存空间。当对s1 append一个值时,此时s1的arry在第6个元素空间写入100,同时len变为了6。所以s1输出

                s1:  [0 1 2 3 4 100]
                复制

                而,此时s的len仍然是5,所以s输出时只会输出前5个元素空间的内容,输出

                  s:  [0 1 2 3 4]
                  复制

                  到这里,通过slice的结构定义,我们已经清楚的知道了上述案例中的输出的原因了。我们再看下下面这个场景s和s3分别输出什么呢?

                    package main

                    import "fmt"

                    func main() {
                    s := make([]int, 5, 5)
                    for i := 0; i < 5; i++ {
                    s[i] = i
                    }
                    apAndChange(s)
                    fmt.Println("s: ", s)
                    }

                    func apAndChange(s3 []int) {
                    s3 = append(s3, 100)
                    s3[0] = 100
                    fmt.Println("s1: ", s3)
                    }
                    复制

                    注意,在这个例子中与前两个案例不同,s的cap和len相等都为5。

                    该代码示例的输出为:

                      s3:  [100 1 2 3 4 100]
                      s: [0 1 2 3 4]
                      复制

                      产生这个输出的原因与append函数的实现有关,我们先看下下append逻辑中发生扩容时的源码一段(growslice):

                      在growslice函数最后,通过malloc方法新申请了一片内存空间,而且把原来的数据通过memmove方法复制到新的空间。而如果slice没有发生扩容,则只需要将新插入的值插入到array最后

                      这就可以回答案例三中,先append后再修改s3[0]不会影响s[0]了,在案例三中s的cap与len相等,当append一个元素时,必定会发生扩容。

                      我们可以通过如下代码可以了解下slice的扩容策略是怎样的?

                      这一段表示slice的扩容逻辑,其策略为:

                      1.  如果新扩容后的cap大于原来的cap*2则直接使用新扩容后的cap

                      2. 如果新扩容后的cap小于等于原来的cap*2

                        1. 原来的cap小于1024,则直接扩容为原来cap的两倍大小

                        2. 如果cap>1024,每次增加原来cap的1/4(newcap = cap*1.25)

                      而扩容后还需要考虑内存对齐,其代码如下:

                      这里我们需要注意roundsize,会向上取整。这个“上”是多少呢?我们可以参考go的对象字节占用表,路径为 runtime/sizeclasses.go。以下截图只截取了一部分源码,有兴趣的同学可自行前往查看。

                      例如某个slice扩容后占用的字节为42,向上取整后实际占用的内存空间则为48。有关为什么要内存对齐,这个后续单独用一节来讲解。

                      有关slice的介绍就到这里了,通过案例1和案例2,大家可以清楚的避开在函数穿时,由于误对slice的处理导致影响函数外的值。通过案例3,我们也了解了append的实现细节,大家在使用slice时,尽量提前申请append的cap,避免在使用过程中由于append操作的扩容(新申请内存,并拷贝数据)带来影响程序运行效率。

                      往期回顾:

                      go语言系列1 - Goroutine的实现与调度

                      go语言系列2 - 剖析Channel的实现原理

                      go语言系列3 - Channel应用流水线模型

                      go语言系列4 - Goroutine、channel的使用还有话说

                      文章转载自栋总侃技术,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                      评论