go切片探索

152 阅读8分钟

突发奇想开始研究切片

func main() {
	var list = []int{1, 2, 3}
    add(list)
	fmt.Println(list) //[1 2 3]
    addp(&list)
	fmt.Println(list) //[1 2 3 5]
}

func add(list []int) {
	list = append(list, 4)
	fmt.Printf("--%v\n", list) //--[1 2 3 4]
}
func addp(list *[]int) {
	*list = append(*list, 5)
	fmt.Printf("--%v\n", *list) //--[1 2 3 5]
}

追加系列操作,其实不难理解,你要把切片的指针传进函数中,才能在函数内修改函数外的切片变量

func main() {
	var list = []int{1, 2, 3}
    del(list)
	fmt.Println(list) //[1 3 3]
	delp(&list)
	fmt.Println(list) //[1 3]
}

func del(list []int){
	list = append(list[:1], list[2:]...)
	fmt.Printf("--%v\n", list) //--[1 3]
}
func delp(list *[]int){
	*list = append((*list)[:1], (*list)[2:]...)
	fmt.Printf("--%v\n", *list) //--[1 3]
}

删除系列的有点意思,我们可以看到,函数del虽然我们传入参数时传的是切片变量,但我们依然在函数内修改到了函数外的切片变量,list[1]从2被修改成了3 当我们在函数的参数中传入切片变量时,go会在函数内自动声明一个新的切片变量供函数内使用,但是新声明的这个切片指向的实际存储空间和函数外切片变量指向的空间是一样的 我们看具体点

func main() {
	var list = []int{1, 2, 3}
	fmt.Printf("&list : %p\n", &list)       //&list    : 0xc0000044a0
	fmt.Printf("list : %p\n", list)         //list     : 0xc00000c400
	fmt.Printf("&list[0] : %p\n", &list[0]) //&list[0] : 0xc00000c400
	fmt.Printf("&list[1] : %p\n", &list[1]) //&list[1] : 0xc00000c408
	del(list)
	fmt.Println(list) //[1 3 3]
}

func del(list []int){
	fmt.Printf("--&list : %p\n", &list)       //--&list    : 0xc000004500
	fmt.Printf("--list : %p\n", list)         //--list     : 0xc00000c400
	fmt.Printf("--&list[0] : %p\n", &list[0]) //--&list[0] : 0xc00000c400
	fmt.Printf("--&list[1] : %p\n", &list[1]) //--&list[1] : 0xc00000c408
	list = append(list[:1], list[2:]...)
	fmt.Printf("--%v\n", list) //--[1 3]
}

上例中可以很明显的看到函数内外的切片变量的指针是不同的,也就是说在函数内是新声明了一个切片变量来使用,但是函数内新声明的切片指向的内存位置与函数外的切片变量是一样的,所以在函数内对这块内存空间做修改操作还是会影响到函数外的切片变量的。

虽然可以做到在函数内修改函数外切片变量的值,但原则上不建议你这样隐式地在函数内修改函数外的变量,可读性太差

另外我们还可以看到如果你fmt.Printf("list : %p\n", list)这样直接打印一个切片的指针,实际打印出来的是这个切片变量指向的存储空间的地址,而不是这个切片变量自身的地址。

还有一种很奇怪的操作

func main() {
	var list = []int{1, 2}
	fmt.Println(cap(list)) //2
	list = append(list, 3)
	list2 := list
	fmt.Printf("%p\n", list)  //0xc00000c400
	fmt.Printf("%p\n", list2) //0xc00000c400
	list = append(list, 4)
	list2 = append(list2, 5)
	fmt.Printf("%p\n", list)  //0xc00000c400
	fmt.Printf("%p\n", list2) //0xc00000c400
	fmt.Println(list)         //[1 2 3 5]
	fmt.Println(list2)        //[1 2 3 5]
	fmt.Println(cap(list))    //4
}

两个list打出来都是[1 2 3 5],为什么呢,我们可以看到append前后4个地址打出来都是相同的,这说明list和list2实际指向的存储空间是同一个存储空间,所以他们对于这块存储空间的修改会互相影响,第二个append用5替换掉了4。 可见,直接将一个切片变量等于另一个切片变量,这种操作和将切片作为参数传入函数时是一样的,新声明的切片变量会与旧的切片变量指向相同的实际存储空间。

func main() {
	var list = []int{1, 2}
	fmt.Println(cap(list)) //2
	list2 := list
	fmt.Printf("%p\n", list)  //0xc000094080
	fmt.Printf("%p\n", list2) //0xc000094080
	list = append(list, 4)
	list2 = append(list2, 5)
	fmt.Printf("%p\n", list)  //0xc000092180
	fmt.Printf("%p\n", list2) //0xc0000921a0
	fmt.Println(list)         //[1 2 4]
	fmt.Println(list2)        //[1 2 5]
	fmt.Println(cap(list))    //4
}

现在这个表现应该比较符合各位的预期,实际上和上面相比,这次只是去掉了第4行的list = append(list, 3)操作而已,为什么表现就会不一样呢? 观察打印出来的4个指针,可以看出,在append前,两个切片变量指向的存储地址是一样的,但在append之后,两个切片变量指向的存储地址全都不一样了,这是因为在append的时候切片的原有容量cap为2,已经容不下第三个元素了,切片就需要申请一个更大的存储空间,并将原有切片内容拷贝过去,这就使得经过append后,两个切片变量指向的都是新的存储空间了,也就不会互相影响了。

另外上面几个例子还隐含了一个小知识点就是,使用var list = []int{1, 2, 3}这种字面量形式来声明切片变量,切片变量的cap容量会和你声明的字面量数量相同。

func main() {
	var list = []int{1}
	fmt.Printf("%p\n", &list)    //0xc0000044a0
	fmt.Printf("%p\n", &list[0]) //0xc0000140a0
	assignment(&list)
	fmt.Println(list) //[1]
}

func assignment(list *[]int){
	fmt.Printf("---%p\n", &list)       //---0xc000006030
	fmt.Printf("---%p\n", &(*list)[0]) //---0xc0000140a0
	list = &[]int{1, 2}
	fmt.Printf("---%p\n", &(*list)[0]) //---0xc0000140d0
}

上面的代码中,我们声明一个[1]的切片,并将此切片的指针传入方法中,在方法中对此指针赋值,但是并没有改动到函数外面的切片 这是因为go在函数传参的时候使用的全部都是值传递,通过打印出来的地址可以看到,函数内外的切片他们的变量地址是不一样的,虽然我们传参的时候传入的是&list,但函数内外的&list地址打出来就是不一样的,只是变量所指向的存储空间是一样的。 也即,在传参时,go将我们传入的切片指针变量复制了一份供函数内使用,复制出来的这个函数内的切片指针变量也指向相同的实际存储空间。 在函数内,我们对函数内的切片指针变量赋值,改变了它所指向的实际存储空间的位置,但这个行为真正改变的,是函数内的切片指针变量,完全没有影响到函数外的切片指针变量。

通过上面的几个例子我们还可以看出来一个知识点,我这里再用一个例子给大家演示

func main() {
	var list = []int{1}
	assignment(&list)
	fmt.Println(list) //[2]

	assignment2(list)
	fmt.Println(list) //[3]
}

func assignment(list *[]int){
	(*list)[0] = 2
}

func assignment2(list []int){
	list[0] = 3
}

本例我就不打印指针了,相信大家不需要指针也看得懂。 方法 assignment 和 assignment2 只有一个差别,就是传入的参数是不是指针,从打印我们可以看到,两个方法对切片变量的修改都成功了,这说明这两种操作在本质上是没有区别的。 切片变量在传参时不论是变量还是指针形式,go都会复制一份再在函数内使用,但复制后的变量或指针指向的实际存储空间都和函数外的切片变量是相同的。 故,函数有切片参数时,传指针与变量并没有实际差别,怎么方便怎么来~ ** 这个时候让我们回头看一眼第一个例子 我们刚才实验的结果是,传参的时候不管传入的参数是变量还是指针变量,进到函数里面都是一个新的变量【或指针变量】,然后指向和函数外变量相同的实际存储空间。 所以例1不管是add方法还是addp方法,实际参数传入函数内后,都会是一个新的专供函数内使用的变量,并且指向与函数外相同的实际存储空间。 不管是add还是addp都是在函数内做append操作,我们知道这个append会导致切片的长度len超过容量cap,go会申请一块新的cap为原cap两倍的内存空间,然后将原切片的内容拷贝到新申请的内存空间中,把切片变量指向内存空间的指针也移过去指向新的内存空间,最后再去做这个append操作。

到这一步为止,add和addp函数中的操作都是一样的,不一样的是,在addp对指针做操作的时候,go也会将函数外的切片变量指向的实际存储空间改为这个新申请的内存空间。下面把指针打出来看一下就知道了

func main() {
	var list = []int{1, 2, 3}
	fmt.Printf("%p\n", &list)    //0xc000004480
	fmt.Printf("%p\n", &list[0]) //0xc000010420
	add(list)
	fmt.Printf("%p\n", &list[0]) //0xc000010420

	addp(&list)
	fmt.Printf("%p\n", &list[0]) //0xc00000c330
}

func add(list []int) {
	fmt.Printf("--%p\n", &list)    //--0xc0000044c0
	fmt.Printf("--%p\n", &list[0]) //--0xc000010420
	list = append(list, 4)
	fmt.Printf("--%p\n", &list)    //--0xc0000044c0
	fmt.Printf("--%p\n", &list[0]) //--0xc00000c300
}

func addp(list *[]int) {
	fmt.Printf("--%p\n", list)        //--0xc000004480
	fmt.Printf("--%p\n", &(*list)[0]) //--0xc000010420
	*list = append(*list, 5)
	fmt.Printf("--%p\n", list)        //--0xc000004480
	fmt.Printf("--%p\n", &(*list)[0]) //--0xc00000c330
}

从指针上就能很明显地看出,在两个方法的append之后,切片变量指向的实际存储空间都变了,但是只有addp方法,会带着函数外的切片变量一起改变,从而影响了函数外的切片变量。