zoukankan      html  css  js  c++  java
  • Go切片实现

    切片简单介绍

    slice 的底层数据是数组,slice 是对数组的封装,它描述一个数组的片段。两者都可以通过下标来访问单个元素。

    数组是定长的,长度定义好之后,不能再更改。而切片则非常灵活,它可以动态地扩容。切片的类型和长度无关。

    数组就是一片连续的内存, slice 实际上是一个结构体,包含三个字段:长度、容量、底层数组。

    // runtime/slice.go
    type slice struct {
    	array unsafe.Pointer // 元素指针
    	len   int // 长度 
    	cap   int // 容量
    }  

    slice 的数据结构如下:

    切片数据结构

    切片的创建和初始化

    在 Golang 中可以通过多种方式创建和初始化切片。是否提前知道切片所需的容量通常会决定如何创建切片。

    通过 make() 函数创建切片
    使用 Golang 内置的 make() 函数创建切片,此时需要传入一个参数来指定切片的长度:

    // 创建一个整型切片
    // 其长度和容量都是 5 个元素
    slice := make([]int, 5) 

    此时只指定了切片的长度,那么切片的容量和长度相等。也可以分别指定长度和容量:

    // 创建一个整型切片
    // 其长度为 3 个元素,容量为 5 个元素
    slice := make([]int, 3, 5)

    分别指定长度和容量时,创建的切片,底层数组的长度是指定的容量,但是初始化后并不能访问所有的数组元素。

    注意,Golang 不允许创建容量小于长度的切片,当创建的切片容量小于长度时会在编译时刻报错:

    // 创建一个整型切片
    // 使其长度大于容量
    myNum := make([]int, 5, 3)

    编译上面的代码,会收到下面的编译错误:

    len larger than cap in make([]int)

    通过字面量创建切片
    另一种常用的创建切片的方法是使用切片字面量,这种方法和创建数组类似,只是不需要指定[]运算符里的值。初始的长度和容量会基于初始化时提供的元素的个数确定:

    // 创建字符串切片
    // 其长度和容量都是 3 个元素
    myStr := []string{"Jack", "Mark", "Nick"}
    // 创建一个整型切片
    // 其长度和容量都是 4 个元素
    myNum := []int{10, 20, 30, 40}

    当使用切片字面量创建切片时,还可以设置初始长度和容量。要做的就是在初始化时给出所需的长度和容量作为索引。下面的语法展示了如何使用索引方式创建长度和容量都是100个元素的切片:

    // 创建字符串切片
    // 使用空字符串初始化第 100 个元素
    myStr := []string{99: ""}

    区分数组的声明和切片的声明方式
    当使用字面量来声明切片时,其语法与使用字面量声明数组非常相似。二者的区别是:如果在 [] 运算符里指定了一个值,那么创建的就是数组而不是切片。只有在 [] 中不指定值的时候,创建的才是切片。看下面的例子:

    // 创建有 3 个元素的整型数组
    myArray := [3]int{10, 20, 30}
    // 创建长度和容量都是 3 的整型切片
    mySlice := []int{10, 20, 30}
    

      

    slice基本实现

    slice结构大致存储了三个部分,第一部分为指向底层数组的指针ptr,其次是切片的大小len和切片的容量cap

          +--------+
          |        |
          |  ptr   |+------------+-------+-----------+
          |        |                     |           |
          +--------+                     |           |
          |        |                     |           |
          |        |                     |           |
          |  len 5 |                     |           |
          |        |                     |           |
          +--------+                     v           v
          |        |             +-----+-----+-----+-----+----+
          |        |             |     |     |     |     |    |
          |  cap 5 |     [5]int  |  0  |  1  |  2  |  3  | 4  |
          |        |             +-----+-----+-----+-----+----+
          +--------+
       
    arr := [5]int{0,1,2,3,4}
    slice := arr[1:4] 

    有一个数组arr是一个包含五个int类型的结构,它的切片slice只是从其取了 1到3这几个数字。我们同样可以再生成一个切片 slice2 := arr[2:5], 所取的就是数组后面的连续块。他们共同使用arr作为底层的结构,可以看见共用了数字的第3,4个元素。修改其中任何一个,都能改变两个切片的值。

    func main() {
    
        arr := [5]int{0, 1, 2, 3, 4}
        fmt.Println(arr)
    
        slice := arr[1:4]
        slice2 := arr[2:5]
    
        fmt.Printf("arr %v, slice1 %v, slice2 %v, %p %p %p
    ", arr, slice, slice2, &arr, &slice, &slice2)
    
        fmt.Printf("arr[2]%p slice[1] %p slice2[0]%p
    ", &arr[2], &slice[1], &slice2[0])
    
        arr[2] = 2222
    
        fmt.Printf("arr %v, slice1 %v, slice2 %v
    ", arr, slice, slice2)
    
    
        slice[1] = 1111
    
        fmt.Printf("arr %v, slice1 %v, slice2 %v
    ", arr, slice, slice2)
    
    }

    输出的值为:

    [0 1 2 3 4]
    arr [0 1 2 3 4], slice1 [1 2 3], slice2 [2 3 4], 0xc42006e0c0 0xc4200660c0 0xc4200660e0
    arr[2]0xc42006e0d0 slice[1] 0xc42006e0d0 slice2[0]0xc42006e0d0
    arr [0 1 2222 3 4], slice1 [1 2222 3], slice2 [2222 3 4]
    arr [0 1 1111 3 4], slice1 [1 1111 3], slice2 [1111 3 4]

    由此可见,数组的切片,只是从数组上切一段数据下来,不同的切片,其实是共享这些底层的数据数据。不过这些切片本身是不一样的对象,其内存地址都不一样。

    从数组中切一块下来形成切片很好理解,有时候我们用make函数创建切片,实际上golang会在底层创建一个匿名的数组。如果从新的slice再切,那么新创建的两个切片都共享这个底层的匿名数组。

    func main() {
    
        slice := make([]int, 5)
        for i:=0; i<len(slice);i++{
            slice[i] = i
        }
        fmt.Printf("slice %v 
    ", slice)
    
        slice2 := slice[1:4]
        fmt.Printf("slice %v, slice2 %v 
    ", slice, slice2)
    
        slice[1] = 1111
        fmt.Printf("slice %v, slice2 %v 
    ", slice, slice2)
    }

    输出如下:

    slice [0 1 2 3 4] 
    slice [0 1 2 3 4], slice2 [1 2 3] 
    slice [0 1111 2 3 4], slice2 [1111 2 3]
    更多例子如下:
    package main
    
    import "fmt"
    
    func main() {
    	slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    	s1 := slice[2:5]
    	s2 := s1[2:6:7]
    
    	s2 = append(s2, 100)
    	s2 = append(s2, 200)
    
    	s1[2] = 20
    
    	fmt.Println(s1)
    	fmt.Println(s2)
    	fmt.Println(slice)
    } 

    结果:

    [2 3 20]
    [4 5 6 7 100 200]
    [0 1 2 3 20 5 6 7 100 9]

    s1 从 slice 索引2(闭区间)到索引5(开区间,元素真正取到索引4),长度为3,容量默认到数组结尾,为8。 s2 从 s1的索引2(闭区间)到索引6(开区间,元素真正取到索引5),容量到索引7(开区间,真正到索引6),为5。

    slice origin

    接着,向 s2 尾部追加一个元素 100:

    s2 = append(s2, 100)

    s2 容量刚好够,直接追加。不过,这会修改原始数组对应位置的元素。这一改动,数组和 s1 都可以看得到。

    append 100

    再次向 s2 追加元素200:

    s2 = append(s2, 100)

    这时,s2 的容量不够用,该扩容了。于是,s2 另起炉灶,将原来的元素复制新的位置,扩大自己的容量。并且为了应对未来可能的 append 带来的再一次扩容,s2 会在此次扩容的时候多留一些 buffer,将新的容量将扩大为原始容量的2倍,也就是10了。

    append 200

    最后,修改 s1 索引为2位置的元素:

    s1[2] = 20

    这次只会影响原始数组相应位置的元素。它影响不到 s2 了,人家已经远走高飞了。

    s1[2]=20

    再提一点,打印 s1 的时候,只会打印出 s1 长度以内的元素。所以,只会打印出3个元素,虽然它的底层数组不止3个元素。




    slice的复制

    既然slice的创建依赖于数组,有时候新生成的slice会修改,但是又不想修改原来的切片或者数组。此时就需要针对原来的切片进行复制了。

    func main() {
    
        slice := []int{0, 1, 2, 3, 4}
    
        slice2 := slice[1:4]
    
        slice3 := make([]int, len(slice2))
    
        for i, e := range slice2 {
            slice3[i] = e
        }
    
        fmt.Printf("slice %v, slice3 %v 
    ", slice, slice3)
    
        slice[1] = 1111
    
        fmt.Printf("slice %v, slice3 %v 
    ", slice, slice3)
    } 

    输出:

    slice [0 1 2 3 4], slice3 [1 2 3] 
    slice [0 1111 2 3 4], slice3 [1 2 3]

    由此可见,新创建的slice3,不会因为slice和slice2的修改而改变slice3。复制很有用,因此golang实现了一个内建的函数copy, copy有两个参数,第一个参数是复制后的对象,第二个是复制前的数组切片对象。

    func main() {
    
        slice := []int{0, 1, 2, 3, 4}
        slice2 := slice[1:4]
    
        slice4 := make([]int, len(slice2))
    
        copy(slice4, slice2)
    
        fmt.Printf("slice %v, slice4 %v 
    ", slice, slice4)
        slice[1] = 1111
        fmt.Printf("slice %v, slice4 %v 
    ", slice, slice4)
    }

    slice4是从slice2中copy生成,slice和slice4底层的匿名数组是不一样的。因此修改他们不会影响彼此。

    slice 追加

    append 简介

    创建复制切片都是常用的操作,还有一个追加元素或者追加数组也是很常用的功能。golang提供了append函数用于给切片追加元素。append第一个参数为原切片,随后是一些可变参数,用于将要追加的元素或多个元素。

    当添加元素超过容量时时会发生扩容。

    func main() {
    
        slice := make([]int, 1, 2)
        slice[0] = 111
    
        fmt.Printf("slice %v, slice addr %p, len %d, cap %d 
    ", slice, &slice, len(slice), cap(slice))
    
        slice = append(slice, 222)
        fmt.Printf("slice %v, slice addr %p, len %d, cap %d 
    ", slice, &slice, len(slice), cap(slice))
    
        slice = append(slice, 333)
        fmt.Printf("slice %v, slice addr %p, len %d, cap %d 
    ", slice, &slice, len(slice), cap(slice))
    
    }

    输出结果为:

    slice [111], slice addr 0xc4200660c0, len 1, cap 2 
    slice [111 222], slice addr 0xc4200660c0, len 2, cap 2 
    slice [111 222 333], slice addr 0xc4200660c0, len 3, cap 4

    切片容量

    无论数组还是切片,都有长度限制。也就是追加切片的时候,如果元素正好在切片的容量范围内,直接在尾部追加一个元素即可。如果超出了最大容量,再追加元素就需要针对底层的数组进行复制和扩容操作了。

    这里有一个切片容量的概念,从数组中切数据,切片的容量应该是切片的最后一个数据,和数组剩下元素的大小,再加上现有切片的大小。

    数组 [0, 1, 2, 3, 4] 中,数组有5个元素。如果切片 s = [1, 2, 3],那么3在数组的索引为3,也就是数组还剩最后一个元素的大小,加上s已经有3个元素,因此最后s的容量为 1 + 3 = 4。如果切片是
    s1 = [4],4的索引再数组中是最大的了,数组空余的元素为0,那么s1的容量为 0 + 1 = 1。具体如下表:

    切片切片字面量数组剩下空间长度容量
    s[1:3] [1 2] 2 2 4
    s[1:1] [] 4 0 4
    s[4:4] [] 1 0 1
    s[4:5] [4] 0 1 1

    尽管上面的第二个和第三个切片的长度一样,但是他们的容量不一样。容量与最终append的策略有关系。

    append简单实现

    我们已经知道,切片都依赖底层的数组结构,即使是直接创建的切片,也会生成一个匿名的数组。使用append时候,本质上是针对底层依赖的数组进行操作。如果切片的容量大于长度,给切片追加元素其实是修改底层数中,切片元素后面的元素。如果容量满了,就不能在原来的数组上修改,而是要创建一个新的数组,当然golang是通过创建一个新的切片实现的,因为新切片必然也有一个新的数组,并且这个数组的长度是原来的2倍,使用动态规划算法的简单实现。

    func main() {
    
        arr := [3]int{0, 1, 2}
    
        slice := arr[1:2]
    
        fmt.Printf("arr %v len %d, slice %v  len %d, cap %d, 
    ", arr, len(arr), slice, len(slice), cap(slice))
    
        slice[0] = 333
    
        fmt.Printf("arr %v len %d, slice %v  len %d, cap %d, 
    ", arr, len(arr), slice, len(slice), cap(slice))
    
        slice = append(slice, 4444)
    
        fmt.Printf("arr %v len %d, slice %v  len %d, cap %d, 
    ", arr, len(arr), slice, len(slice), cap(slice))
    
        slice = append(slice, 5555)
    
        fmt.Printf("arr %v len %d, slice %v  len %d, cap %d, 
    ", arr, len(arr), slice, len(slice), cap(slice))
    
        slice[0] = 333
    
        fmt.Printf("arr %v len %d, slice %v  len %d, cap %d, 
    ", arr, len(arr), slice, len(slice), cap(slice))
    } 

    输出:

    arr [0 1 2] len 3, slice [1]  len 1, cap 2, 
    arr [0 333 2] len 3, slice [333]  len 1, cap 2, 
    arr [0 333 444] len 3, slice [333 444]  len 2, cap 2, 
    arr [0 333 444] len 3, slice [333 444 555]  len 3, cap 4, 
    arr [0 333 444] len 3, slice [333 444 555]  len 3, cap 4,

    小于容量的append

    重输出,我们来画一下这个动态过程的图示:

           +----+----+----+                           +----+----+----+                             +----+----+----+
           |    |    |    |                           |    |    |    |                             |    |    |    |
     arr   | 0  |  1 |  2 |                     arr   | 0  |333 | 2  |                       arr   | 0  |333 |444 |
           +----+----+----+                           +----+----+----+                             +----+----+----+
                   ^                                          ^                                            ^    ^
                   |                                          |                                            |    |
                   |                                          |                                            |    |
                   |                 slic0] = 333             |              slice = append(slice, 444)    +----+
                   |               +----------------->        |                +---------------->          |
                   |                                          |                                            |
                +--+--+----+----+                          +--+--+----+----+                            +--+--+----+----+
                |     |    |    |                          |     |    |    |                            |     |    |    |
                | p   | 1  | 2  |                          | p   | 1  | 2  |                            | p   | 2  | 2  |
                +-----+----+----+                          +-----+----+----+                            +-----+----+----+
    
                slice :=arr[1:2]                           slice :=arr[1:2]                             slice :=arr[1:2]

    arr 是一个含有三个元素的数组,slice从arr中切了一个元素,由于切片的最后一个元素1是数组的索引是1,距离数组的最大长度还是1,因此slice的容量为2。当修改slice的第一个元素,由于slice底层是arr数组,因此arr的第二个元素也相应被修改。使用append方法给slice追加元素的时候,由于slice的容量还未满,因此等同于扩展了slice指向数组的内容,可以理解为重新切了一个数组内容附给slice,同时修改了数组的内容。

    超出容量的append

    如果接着append一个元素,那么数组肯定越界。此时append的原理大致如下:

    1. 创建一个新的临时切片t,t的长度和slice切片的长度一样,但是t的容量是slice切片的2倍,一个动态规划的方式。新建切片的时候,底层也创建了一个匿名的数组,数组的长度和切片容量一样。
    2. 复制s里面的元素到t里,即填入匿名数组中。然后把t赋值给slice,现在slice的指向了底层的匿名数组。
    3. 转变成小于容量的append方法。
           +----+----+----+                                +----+----+----+----+----+----+
           |    |    |    |                                |    |    |    |    |    |    |
     arr   | 0  |333 |444 |                                | 333| 444|    |    |    |    |
           +----+----+----+                                +----+----+----+----+----+----+
                   ^    ^                                    ^     ^
                   |    |                                    |     |
                   |    |                                    +-----+
                   +----+           +--------------->        |
                   |                                         |
                   |                                         +
                +--+--+----+----+                          +-----+-----+-----+
                |     |    |    |                          |     |     |     |
                | p   | 2  | 2  |                          | p   |  2  |  6  |
                +-----+----+----+                          +-----+-----+-----+
    
                slice :=arr[1:2]                          t := make([]int, len=2, cap=6)
    
                                                                      +
                                                                      |
                                                                      |
                                                                      |
                                                                      |
                                                                      v
    
    
                                                           +----+----+----+----+----+----+
                                                           |    |    |    |    |    |    |
                                                           | 333| 444|555 |    |    |    |
                                                           +----+----+----+----+----+----+
                                                             ^         ^
                                                             |         |
                                                             +----+----+
                                                             |
                                                             |
                                                             +
                                                           +-----+-----+-----+
                                                           |     |     |     |
                                                           | p   |  3  |  6  |
                                                           +-----+-----+-----+
    
                                                            slice = t

    上面的图示描述了大于容量的时候append的操作原理。新生成的切片其依赖的数组和原来的数组就没有关系了,因此在修改新的切片元素,旧的数组也不会有关系。至于临时的切片t,将会被golang的gc回收。当然arr或它衍生的切片都没有应用的时候,也会被gc所回收。

    slice和array的关系十分密切,通过两者的合理构建,既能实现动态灵活的线性结构,也能提供访问元素的高效性能。当然,这种结构也不是完美无暇,共用底层数组,在部分修改操作的时候,可能带来副作用,同时如果一个很大的数组,那怕只有一个元素被切片应用,那么剩下的数组都不会被垃圾回收,这往往也会带来额外的问题。

    作为函数参数的切片

    直接改变切片

    切片传递的时候,传的是数组的值,等效于从原始切片中再切了一次。原始切片slice和参数s切片的底层数组是一样的。因此修改函数内的切片,也就修改了数组。

                                                    +-----+----+-----+
                                                    |     |    |     |
                     +-----------------------------+| p   |  3 |  3  |
                     |          +                   +-----+----+-----+
                     |          |
                     |          |                     s
                     |          |
                     |          |
                     v          v
                   +----+----+-----+
                   |    |    |     |
           arr     | 0  |  1 |  2  |
                   +----+----+-----+
                     ^           ^
                     |           |
                     |           |
                     +-----------+
                     |
                     |
                   +-+--+----+-----+
                   |    |    |     |
                   |  p |  3 |  3  |
                   +----+----+-----+
    
    
                      slice

    例如下面的代码:

      slice := make([]int, 2, 3)
        for i := 0; i < len(slice); i++ {
            slice[i] = i
        }
    
        fmt.Printf("slice %v %p 
    ", slice, &slice)
    
        ret := changeSlice(slice)
        fmt.Printf("slice %v %p, ret %v 
    ", slice, &slice, ret)
    
        ret[1] = 1111
    
        fmt.Printf("slice %v %p, ret %v 
    ", slice, &slice, ret)
    }
    
    func changeSlice(s []int) []int {
        fmt.Printf("func s %v %p 
    ", s, &s)
        s = append(s, 3)
        return s
    } 

    输出:

    slice [0 1] 0xc42000a1e0 
    func s [0 1] 0xc42000a260 
    slice [0 1] 0xc42000a1e0, ret [0 1 3] 
    slice [0 1111] 0xc42000a1e0, ret [0 1111 3]

    从输出可以看出,当slice传递给函数的时候,新建了切片s。在函数中给s进行了append一个元素,由于此时s的容量足够到,并没有生成新的底层数组。当修改返回的ret的时候,ret也共用了底层的数组,因此修改ret的原始,相应的也看到了slice的改变。

    append 操作

    如果在函数内,append操作超过了原始切片的容量,将会有一个新建底层数组的过程,那么此时再修改函数返回切片,应该不会再影响原始切片。例如下面代码:

     func main() {
        slice := make([]int, 2, 2)
        for i := 0; i < len(slice); i++ {
            slice[i] = i
        }
    
        fmt.Printf("slice %v %p 
    ", slice, &slice)
    
        ret := changeSlice(slice)
        fmt.Printf("slice %v %p, ret %v 
    ", slice, &slice, ret)
    
        ret[1] = -1111
    
        fmt.Printf("slice %v %p, ret %v 
    ", slice, &slice, ret)
    }
    
    func changeSlice(s []int) []int {
        fmt.Printf("func s %v %p 
    ", s, &s)
        s[0] = -1
        s = append(s, 3)
        s[1] =  1111
        return s
    }

    输出:

    slice [0 1] 0xc42000a1a0 
    func s [0 1] 0xc42000a200 
    slice [-1 1] 0xc42000a1a0, ret [-1 1111 3] 
    slice [-1 1] 0xc42000a1a0, ret [-1 -1111 3]

    从输出可以很清楚的看到了我们的猜想。 即函数中先改变s第一个元素的值,由于slice和s都共用了底层数组,因此无论原始切片slice还是ret,第一个元素都是-1.然后append操作之后,因为超出了s的容量,因此会新建底层数组,虽然s变量没变,但是他的底层数组变了,此时修改s第一个元素,并不会影响原始的slice切片。也就是slice[1]还是1,而ret[1]则是-1。最后在外面修改ret[1]为 -1111,也不会影响原始的切片slice。

    通过上面的分析,我们大致可以下结论,slice或者array作为函数参数传递的时候,本质是传值而不是传引用。传值的过程复制一个新的切片,这个切片也指向原始变量的底层数组。(个人感觉称之为传切片可能比传值的表述更准确)。函数中无论是直接修改切片,还是append创建新的切片,都是基于共享切片底层数组的情况作为基础。也就是最外面的原始切片是否改变,取决于函数内的操作和切片本身容量。

    传引用方式

    array和slice作为参数传递的过程基本上是一样的,即传递他们切片。有时候我们需要处理传递引用的形式。golang提供了指针很方便实现类似的功能。

    func main() {
        slice := []int{0, 1}
        fmt.Printf("slice %v %p 
    ", slice, &slice)
    
        changeSlice(&slice)
        fmt.Printf("slice %v %p 
    ", slice, &slice)
    
        slice[1] = -1111
    
        fmt.Printf("slice %v %p 
    ", slice, &slice)
    }
    
    func changeSlice(s *[]int) {
        fmt.Printf("func s %v %p 
    ", *s, s)
        (*s)[0] = -1
        *s = append(*s, 3)
        (*s)[1] =  1111
    }

    输出如下:

    slice [0 1] 0xc42000a1e0 
    func s [0 1] 0xc42000a1e0 
    slice [-1 1111 3] 0xc42000a1e0 
    slice [-1 -1111 3] 0xc42000a1e0

    从输出可以看到,传递给函数的是slice的指针,函数内对对s的操作本质上都是对slice的操作。并且也可以从函数内打出的s地址看到,至始至终就只有一个切片。虽然在append过程中会出现临时的切片或数组。

    总结

    golang提供了array和slice两种序列结构。其中array是值类型。slice则是复合类型。slice是基于array实现的。slice的第一个内容为指向数组的指针,然后是其长度和容量。通过array的切片可以出slice,也可以使用make创建slice,此时golang会生成一个匿名的数组。

    因为slice依赖其底层的array,修改slice本质是修改array,而array又是有大小限制,当超过slice的容量,即数组越界的时候,需要通过动态规划的方式创建一个新的数组块。把原有的数据复制到新数组,这个新的array则为slice新的底层依赖。

    数组还是切片,在函数中传递的不是引用,是另外一种值类型,即通过原始变量进行切片传入。函数内的操作即对切片的修改操作了。当然,如果为了修改原始变量,可以指定参数的类型为指针类型。传递的就是slice的内存地址。函数内的操作都是根据内存地址找到变量本身。

     
     
    本文记录整理:
     
    https://github.com/qcrao/Go-Questions/tree/master/%E6%95%B0%E7%BB%84%E5%92%8C%E5%88%87%E7%89%87
    https://www.jianshu.com/p/354fce23b4f0
    https://studygolang.com/articles/9876
  • 相关阅读:
    读《高效能人士的七个习惯》有感
    Springboot中的日志
    fastjson JSON.toJavaObject() 实体类首字母大写属性无法解析问题
    java多线程编程实例
    IDEA插件配置推荐
    Spring Boot 自定义数据源 DruidDataSource
    zookeeper环境搭建
    eureka注册中心的使用
    记事本编码
    Chrome浏览器基本操作
  • 原文地址:https://www.cnblogs.com/-wenli/p/13155395.html
Copyright © 2011-2022 走看看