20210127关于golang切片的append,len,cap,以及插入元素 - ziyouzy/2021blog GitHub Wiki

基础语法:append内置函数参数表首个参数必须是切片,在确保了首个参数是切片的前提下第二个参数可以省略
有个坑,先写在前边吧,是关于多个切片指向同一底层数组从而造成的数据错乱问题:
a := []int{1,2}

b := append(a[0:1], 3)
c := append(a[1:2], 4)
fmt.Println(b)
fmt.Println(c)  

打印的结果为:

b=[1,3]
c=[3,4]

问题出在了b := append(a[0:1], 3)这一句,我们换个方式:

b := append(a, 3)

如果打印a的化,a的值为[1,2,3]
因为append会进行两个操作,先把3追加到a切片,这波操作是通过改变底层数组为[1,2,3]来实现的,
再创建一个[]int的引用类型(b),并让其指向同一个底层数组
重复一遍append的设计理念是和copy对立的,append目的只是实现追加元素,并不会估计底层数组是否发生变化,copy则是为了确保底层数组的不被污染而设计的深拷贝内置函数
ps:append()必须用一个变量接收,不能省略其返回值

转到下一个问题:
来看下append函数的例子:

func TestSlice(t *testing.T) {
var numSlice []int  
fmt.Printf("%v  len:%d  cap:%d  ptr:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice)
    for i := 0; i < 10; i++ {  
        numSlice = append(numSlice, i)  
        fmt.Printf("%v  len:%d  cap:%d  ptr:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice)  
    }  
}

=== RUN   TestSlice
[]  len:0  cap:0  ptr:0x0
[0]  len:1  cap:1  ptr:0xc00001a150
[0 1]  len:2  cap:2  ptr:0xc00001a160
[0 1 2]  len:3  cap:4  ptr:0xc0000143a0
[0 1 2 3]  len:4  cap:4  ptr:0xc0000143a0
[0 1 2 3 4]  len:5  cap:8  ptr:0xc00001c180
[0 1 2 3 4 5]  len:6  cap:8  ptr:0xc00001c180
[0 1 2 3 4 5 6]  len:7  cap:8  ptr:0xc00001c180
[0 1 2 3 4 5 6 7]  len:8  cap:8  ptr:0xc00001c180
[0 1 2 3 4 5 6 7 8]  len:9  cap:16  ptr:0xc000060180
[0 1 2 3 4 5 6 7 8 9]  len:10  cap:16  ptr:0xc000060180
--- PASS: TestSlice (0.00s)
PASS
ok      command-line-arguments  0.002s

由此可知:
首先切片的空值形式是一个独立的存在,除了len和cap都为0之外,pr的值也为0
0x0是16进制的0,指针的值真的是实实在在的“0值”
此外比较重要的是在于cap为0,当第一次对切片进行append操作时cap由0变为1,这和之后的2变4,4变8,8变16是完全不一样的,不是乘法关系,但是也不用太在意这种细枝末节上的逻辑实现
不过当cap大于1024时,每次增长幅度会缩小为4分之1,同时对于不同的数据类型(int、string),拱廊内置的相关逻辑实现也是不一样的
感觉有点tcp阻塞控制的影子,但是他没有cap自动减少的内置业务逻辑实现

然后说说copy函数
通过copy函数可以更加完善的理解“数组是切片的底层”这一设计模式,可以充分避免因为不理解他而造成的问题:
首先是错误的复制方式:

func main() {  
    a := []int{1, 2, 3, 4, 5}  
    b := a  
    fmt.Println(a) //[1 2 3 4 5]  
    fmt.Println(b) //[1 2 3 4 5]  
    b[0] = 1000  
    fmt.Println(a) //[1000 2 3 4 5]  
    fmt.Println(b) //[1000 2 3 4 5]  
}

这样一来a和b都会指向共同的底层数组从而造成各类问题,正确的方式如下:

func main() {  
    // copy()复制切片  
    a := []int{1, 2, 3, 4, 5}  
    c := make([]int, 5, 5)  
    copy(c, a)     //使用copy()函数将切片a中的元素复制到切片c  
    fmt.Println(a) //[1 2 3 4 5]  
    fmt.Println(c) //[1 2 3 4 5]  
    c[0] = 1000  
    fmt.Println(a) //[1 2 3 4 5]  
    fmt.Println(c) //[1000 2 3 4 5]  
}

区别很类似与浅拷贝和深拷贝的区别,同时append()函数也可以用类似的方式实现深拷贝:
sl1 :=[]int{0,1,2,3}
sl2 :=append(sl1,4,5,6) //共用底层数组
sl3 :=append([]int{},sl1) //未共用底层数组

接下来是另一个重点,切片的插入操作
官方给出了官方的操作方式:

    s := []int{1,2,3,4,6,7}  
    s1 :=append(s[:4])    //(:index),s1是一个新的独立切片,但是他和s共享一个底层数组,不过此时s的底层数组并没有被修改  
    s2 :=append([]int{},s[4:])    //(index:)//s2是一个新的独立切片,这样写就不会共享底层数组了,s的底层数组依然没有被修改  
    s1 =append(s1,5)

这样操作会修改s1的值(从{1,2,3,4}变为了{1,2,3,4,5}),以及他的底层数组(从{1,2,3,4,6,7}变为了{1,2,3,4,5,7}),同时因为与s共享底层数组,所以s的底层数组也被修改,导致了s的变化
已通过实际代码验证了此操作,s1 =append(s1,5)这一波操作,5这个值对s1这个切片来说是一次“追加”操作,但是对其底层数组来说是“覆盖”操作,将下标为4所对应的值由6改为了5
如果在此打印s:{1,2,3,4,6,7}变为了{1,2,3,4,5,7}但是无所谓,因为不会影响之后的操作

    s =append(s1,s2)  

s的底层数组依然会被进行覆盖操作,如果在此打印s:{1,2,3,4,5,7}变为了{1,2,3,4,5,6,7}
底层数组由{1,2,3,4,5,7}变为了{1,2,3,4,5,6,7}下标为5对应的值被覆盖,同时添加了下标为6的位置并赋值7

最后是删除的方式:

s := []int{1,2,3,4,4,5}
s1 :=s[:4]    //(:index)
s2 :=s[5:]    //(index+1:)
s =append(s1,s2...)

这种方式,s,s1,s2均共用了同一个底层数组
而最后的append对底层数组来说其实是变成了{1,2,3,4,5,5},但是仅仅是底层数组的状态
或者说如果修改一下:

s := []int{1,2,3,4,4,5}
s1 :=s[:4]    //(:index)
s2 :=s[5:]    //(index+1:)

s3 =append(s1,s2...)

再去分别打印s、s1、s3(已经过实际验证):

s:[1 2 3 4 5 5]
s1:[1 2 3 4]
s3:[1 2 3 4 5]

总之append的返回值赋给谁,谁就是最终的结果
而底层数组的改变是一件事
切片的拼接是底层数组上一层所作的事:
每个独立的切片的当前长度(len)决定了其对底层数组的截取长度
append函数的返回值必然会生成一个新的切片,但是也必然不会生成新的底层数组: 即使是 sl :=append{[]int{},sl_old...},sl内部的引用地址字段的值其实是和“[]int{}”相同的
也就是说返回值的内部引用地址字段必然是和他第一个参数内部引用地址字段相同的
而第二个参数的内部引用地址字段没意义,本身在传入的时候也必须通过...来打散,打散后的元素数量会决定新切片的len字段值,同时对于新切片各个下标所对应的值,就会直接去覆盖底层数组的各个下标的对应值了