切片 | 青训营笔记

78 阅读3分钟

这是我参与「第五届青训营 」笔记创作活动的第1天。

从未接触过Go的我总算在今天下笔写下这篇笔记。本文主要记录了我对切片使用的学习和实践。

数组和切片

  • 创建

    切片和数组的初始化,区别在于[]内数组需要有参数,而切片没有

    //数组 var a = [...]int{0, 1, 2}
    //数组 var a = [3]int{0, 1, 2}
    //切片 var a = []int{0, 1, 2}
    //切片 var a = make([]int,8)
    

    我们除了可以从无到有创建一个切片,还可以由数组或数组的一部分生成一个切片。这里[:]省略上下界,省略的上界或下界将会被补充为原数组的上界或下界,在这里等效于[0:3]。还可以通过[x,y],[x:],[:y]来指定上下界或其中之一。

    var a = [...]int{0, 1, 2}
    b := a[:]
    fmt.Println(a) //[0 1 2]
    fmt.Println(b) //[0 1 2]
    
  • 修改

    切片是数组的一个引用,切片和原数组指向的地址相同,对其一的改动会影响到二者

    a[0] = 1
    b[1] = 2
    b[2] = 3
    fmt.Println(a) //[1 2 3]
    fmt.Println(b) //[1 2 3]
    

    若想得到一个不会改变原数组的切片,则需要用到copy函数

    //使用copy复制的切片不会改变原数组,这是因为底层地址不同
    c := make([]int, 3)
    copy(c, a[:])
    a[0] = 6
    c[0] = 0
    fmt.Println(&c)
    fmt.Println(a) //[6 2 3]
    fmt.Println(c) //[0 2 3]
    

    我们查看指向的地址可以找到其根本的原因

    fmt.Printf("%p\n", &a[0]) //相同
    fmt.Printf("%p\n", &b[0]) //相同
    fmt.Printf("%p\n", &c[0]) //不同
    
  • 扩容

    切片可以被看做动态数组,是因为数组容量是固定的,而切片的容量可以扩充

    //切片容量可以大于长度而数组容量始终和长度相同
    //a = append(a, 4)
    //first argument to append must be a slice; have a (variable of type [3]int)
    fmt.Println(cap(b))//3
    fmt.Println(len(b))//3
    b = append(b, 4)
    //此时容量发生了改变,扩容机制扩容机制:
    //当原slice容量(oldcap)小于256的时候,新slice(newcap)容量为原来的2倍
    //当原slice容量超过256,新slice容量newcap = oldcap+(oldcap+3*256)/4
    fmt.Println(cap(b))//6
    fmt.Println(len(b))//4
    

    在扩充时切片的容量不仅仅只扩充了长度的增量,而会遵循扩容机制(见注释),这应该是由于扩充切片本质上是分配一块新的符合容量的连续地址,预留一定的容量可以避免反复执行这种开销大的工作。

    又由于是新分配的地址,因此切片与原数组的改动不再互相影响,切片新扩容的位自然也是原数组不可达的。

    fmt.Println(a) //[6 2 3]
    fmt.Println(b) //[6 2 3 4]
    //a[4] invalid argument: index 4 out of bounds [0:3]
    //扩展后底层数组地址改变,修改不再互相影响
    a[0] = 7
    b[0] = 8
    fmt.Println(a) //[7 2 3]
    fmt.Println(b) //[8 2 3 4]
    

    此时三者的地址各不相同

    fmt.Printf("%p\n", &a[0]) //不同
    fmt.Printf("%p\n", &b[0]) //不同
    fmt.Printf("%p\n", &c[0]) //不同
    
  • 删除

    切片本身不提供删除操作,但本质上是对这一块连续内存的操作,以删除头元素为例。 append和copy函数是相当方便的操作这块连续内存的方法。

    //删除头元素
    fmt.Printf("%p\n", &b[0])   //0x0
    fmt.Println(b)              //[8 2 3 4]
    b = b[1:]                   //会改变头地址
    fmt.Printf("%p\n", &b[0])   //0x8
    fmt.Println(b)              //[2 3 4]
    b = append(b[:0], b[1:]...) //不会改变头地址
    fmt.Printf("%p\n", &b[0])   //0x8
    fmt.Println(b)              //[3 4]
    b = b[:copy(b, b[1:])]      //不会改变头地址
    fmt.Printf("%p\n", &b[0])   //0x8
    fmt.Println(b)              //[4]