Golang函数内slice进行append时不改变外部值问题

Golang函数内slice进行append时不影响外部值问题

这个标题…在看完这篇文章之后应该就能知道,这么说是不正确的,虽然看起来好像是这样的,哈哈哈哈

问题引入:下面这样一段代码会输出什么?


package main

import "fmt"

func main() {
	arr := make([]int, 3, 4)
	arr[0] = 0
	arr[1] = 1
	arr[2] = 2
	fmt.Printf("main before: len: %d cap:%d data:%+v\n", len(arr), cap(arr), arr)
	ap1(arr)
	fmt.Printf("main ap1 after: len: %d cap:%d data:%+v\n\n", len(arr), cap(arr), arr)
}
func ap1(arr []int) {
	fmt.Printf("ap1 before:  len: %d cap:%d data:%+v\n", len(arr), cap(arr), arr)
	arr[0] = 11
	arr = append(arr, 111)
	fmt.Printf("ap1 after:  len: %d cap:%d data:%+v\n", len(arr), cap(arr), arr)
}

输出:

main before: len: 3 cap:4 data:[0 1 2]
ap1 before:  len: 3 cap:4 data:[0 1 2]
ap1 after:  len: 4 cap:4 data:[11 1 2 111]
main ap1 after: len: 3 cap:4 data:[11 1 2]

可以发现,函数内append之后,外部实参切片的值真的没改变,但到底是没改变,还是 “看不到” 呢?
在Golang参数传递:值传递那篇文章中有说过,slice是值传递,但是slice本身是一个结构体,包含一个指针,指向底层数组,将slice按值传递给函数,在函数内对其修改,影响将会传递到函数外,因为底层的数组被修改了。

type slice struct {
array unsafe.Pointer
len   int
cap   int
}

可以打印一下地址值看看:

func main() {
	arr := make([]int, 3, 4)
	arr[0] = 0
	arr[1] = 1
	arr[2] = 2
	fmt.Printf("main before: len: %d cap:%d data:%+v, slice addr:%p, arr[0] addr:%p\n", len(arr), cap(arr), arr, &arr, &arr[0])
	ap1(arr)
	fmt.Printf("main ap1 after: len: %d cap:%d data:%+v\n\n", len(arr), cap(arr), arr)
}
func ap1(arr []int) {
	fmt.Printf("ap1 before: len: %d cap:%d data:%+v, slice addr:%p, arr[0] addr:%p\n", len(arr), cap(arr), arr, &arr, &arr[0])
	arr[0] = 11
	arr = append(arr, 111)
	fmt.Printf("ap1 after: len: %d cap:%d data:%+v, slice addr:%p, arr[0] addr:%p\n", len(arr), cap(arr), arr, &arr, &arr[0])
}

输出:
在这里插入图片描述
既然都是指向的同一个数组,为什么实参slice输出的内容有些被修改了,有些没被修改呢?一切还要从slice的len这个属性说起,就算cap再大,它也只能感知到len范围内的内容,之外的感知不到的…画图说明如下:
Golang函数内slice进行append时不改变外部值问题_第1张图片
ok,接下来再做几个实验:

func main() {
	arr := make([]int, 3, 4)
	arr[0] = 0
	arr[1] = 1
	arr[2] = 2
	fmt.Printf("main before: len: %d cap:%d data:%+v, slice addr:%p, arr[0] addr:%p\n", len(arr), cap(arr), arr, &arr, &arr[0])
	ap2(arr)
	fmt.Printf("main ap2 after: len: %d cap:%d data:%+v\n\n", len(arr), cap(arr), arr)
	ap3(arr)
	fmt.Printf("main ap3 after: len: %d cap:%d data:%+v\n\n", len(arr), cap(arr), arr)
	ap4(arr)
	fmt.Printf("main ap4 after: len: %d cap:%d data:%+v\n", len(arr), cap(arr), arr)
}
func ap2(arr []int) {
	fmt.Printf("ap2 before:  len: %d cap:%d data:%+v\n", len(arr), cap(arr), arr)
	arr = append(arr, 222)
	arr[0] = 22
	fmt.Printf("ap2 after:  len: %d cap:%d data:%+v\n", len(arr), cap(arr), arr)
}
func ap3(arr []int) {
	fmt.Printf("ap3 before: len: %d cap:%d data:%+v, slice addr:%p, arr[0] addr:%p\n", len(arr), cap(arr), arr, &arr, &arr[0])
	arr = append(arr, 333,333)
	arr[0] = 33
	fmt.Printf("ap3 after: len: %d cap:%d data:%+v, slice addr:%p, arr[0] addr:%p\n", len(arr), cap(arr), arr, &arr, &arr[0])
}
func ap4(arr []int) {
	fmt.Printf("ap4 before: len: %d cap:%d data:%+v, slice addr:%p, arr[0] addr:%p\n", len(arr), cap(arr), arr, &arr, &arr[0])
	arr[0] = 44
	arr = append(arr, 444,444)
	fmt.Printf("ap4 after: len: %d cap:%d data:%+v, slice addr:%p, arr[0] addr:%p\n", len(arr), cap(arr), arr, &arr, &arr[0])
}

输出:
Golang函数内slice进行append时不改变外部值问题_第2张图片

为什么ap3中,向切片中append了两个元素之后,再修改其内容就影响不到实参原切片的内容了呢?

这是因为切片底层的扩容机制,当对切片进行append()操作,若是元素数量超过原有切片的容量,将会使得切片扩容,这就是问题所在。扩容后的切片,本质上是产生一个新的底层数组,并把旧数组的值复制进去,然后旧数组被回收掉。如果在函数内对切片添加元素导致扩容,会导致元素内的切片指向一个新的数组,但是函数外的切片仍然指向原来旧的数组,则将会导致影响无法传递到函数外。如果希望函数内对切片扩容作用于函数外,就需要以指针形式传递切片。

源码:src/runtime/slice.go 中的 growslice 函数中的核心部分。

// src/runtime/slice.go
func growslice(et *_type, old slice, cap int) slice {
	// ...省略部分
	newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		if old.len < 1024 {
			newcap = doublecap
		} else {
			// Check 0 < newcap to detect overflow
			// and prevent an infinite loop.
			for 0 < newcap && newcap < cap {
				newcap += newcap / 4
			}
			// Set newcap to the requested cap when
			// the newcap calculation overflowed.
			if newcap <= 0 {
				newcap = cap
			}
		}
	}
	// ...省略部分
}

指针形式传递切片:【注意切片指针类型寻址的时候要(*指针)[index],因为**[]优先级比较高**】

func main() {
	arr := make([]int, 3, 4)
	arr[0] = 0
	arr[1] = 1
	arr[2] = 2
	fmt.Printf("main before: len: %d cap:%d data:%+v, slice addr:%p, arr[0] addr:%p\n", len(arr), cap(arr), arr, &arr, &arr[0])
	ap5(&arr)
	fmt.Printf("main ap5 after: len: %d cap:%d data:%+v, slice addr:%p, arr[0] addr:%p\n", len(arr), cap(arr), arr, &arr, &arr[0])
}
func ap5(arr *[]int) {
	fmt.Printf("ap5 before: len: %d cap:%d data:%+v, pointer addr:%p, arr[0] addr:%p\n", len(*arr), cap(*arr), *arr, &(arr), &(*arr)[0])
	*arr = append(*arr, 555,555)
	(*arr)[0] = 55
	fmt.Printf("ap5 after: len: %d cap:%d data:%+v, pointer addr:%p, arr[0] addr:%p\n", len(*arr), cap(*arr), *arr, &(*arr), &(*arr)[0])
}

可以看到,在函数内部对slice扩容之后,形参和实参的底层数组都换到了同一个地址
在这里插入图片描述
再做个小实验,前面说slice只能感知到len范围内的内容,之外的感知不到,那要是把之外的换到之内呢?答案是肯定的

func main() {
	arr := make([]int, 3, 4)
	arr[0] = 0
	arr[1] = 1
	arr[2] = 2
	fmt.Printf("main before: len: %d cap:%d data:%+v, slice addr:%p, arr[0] addr:%p\n", len(arr), cap(arr), arr, &arr, &arr[0])
	reverse1(arr)
	fmt.Printf("main reverse1 after: len: %d cap:%d data:%+v\n", len(arr), cap(arr), arr)
	reverse2(arr)
	fmt.Printf("main reverse2 after: len: %d cap:%d data:%+v\n", len(arr), cap(arr), arr)
}
func reverse1(arr []int) {
	fmt.Printf("reverse1 before:  len: %d cap:%d data:%+v\n", len(arr), cap(arr), arr)
	arr = append(arr, 111)
	for i, j := 0, len(arr) - 1; i < j; i++ {
		j = len(arr) - (i + 1)
		arr[i], arr[j] = arr[j], arr[i]
	}
	fmt.Printf("reverse1 after:  len: %d cap:%d data:%+v\n", len(arr), cap(arr), arr)
}
func reverse2(arr []int) {
	fmt.Printf("reverse2 before:  len: %d cap:%d data:%+v\n", len(arr), cap(arr), arr)
	arr = append(arr, 222,22)
	for i, j := 0, len(arr) - 1; i < j; i++ {
		j = len(arr) - (i + 1)
		arr[i], arr[j] = arr[j], arr[i]
	}
	fmt.Printf("reverse2 after:  len: %d cap:%d data:%+v\n", len(arr), cap(arr), arr)
}


Golang函数内slice进行append时不改变外部值问题_第3张图片
再来看点切片的其他知识吧,切片可以用上面实验中用的make的方式生成,也可以从数组中切,那切的话又有什么问题呢?

func main() {
	s := make([]int, 2, 10)
	fmt.Printf("s: %v, s[1] addr: %p\n", s, &s[1])
	b := s[1:3:4] //b的内容对应s的坐标范围:[1,3),len=3-1=2,cap=4-1=3
	fmt.Printf("b: %v, b[0] addr: %p\n", b, &b[0])

	c := append(b, 2)
	fmt.Println("after c := append(b,2):")
	fmt.Println("s:", s)
	fmt.Println("b:", b)
	fmt.Printf("c: %v, len: %v, cap:%v, c[0] addr: %p\n", c, len(c), cap(c), &c[0])

	fmt.Println("由于切片d长度为4,超出b的容量,go会给切片d分配新内存,以后对s的操作都与d无关")
	d := append(b, 4, 5)
	fmt.Println("after d := append(b,4,5):")
	fmt.Println("s:", s)
	fmt.Println("b:", b)
	fmt.Println("c:", c)
	fmt.Printf("d: %v, len: %v, cap:%v, d[0] addr:%p\n", d, len(d), cap(d), &d[0])
	s = append(s, 3, 3)
	fmt.Println("after s = append(s,3,3):")
	fmt.Println("s:", s)
	fmt.Println("b:", b)
	fmt.Println("c:", c)
	fmt.Println("d:", d)

	b = append(b, 6)
	fmt.Println("after b = append(b, 6):")
	fmt.Println("s:", s)
	fmt.Println("b:", b)
	fmt.Println("c:", c)
	fmt.Println("d:", d)
	fmt.Println("由于切片b长度变为4,超出b的容量,go会给切片b分配新内存,以后对b的操作都与基于b的切片无关")
	b = append(b, 7)
	fmt.Println("after b = append(b, 7):")
	fmt.Println("s:", s)
	fmt.Printf("b: %v, b[0] addr: %p\n", b, &b[0])
	fmt.Println("c:", c)
	fmt.Println("d:", d)
	b[0] = 8
	fmt.Println("after b[0] = 8:")
	fmt.Println("s:", s)
	fmt.Println("b:", b)
	fmt.Println("c:", c)
	fmt.Println("d:", d)
}

Golang函数内slice进行append时不改变外部值问题_第4张图片
Golang函数内slice进行append时不改变外部值问题_第5张图片

ps:关于copy函数:

copy()将切片s的内容复制给切片s1:返回复制的元素数
若元素不够,则只拷贝一部分
s和s1数据空间相互独立,相互不影响

s := []int{1,2}
s1 := make([]int,len(s)*2,cap(s)*3)

copy(s1,s)
fmt.Println(len(s),cap(s),s)
fmt.Println(len(s1),cap(s1),s1)
//执行结果:
2 2 [1 2]
4 6 [1 2 0 0]

参考

你可能感兴趣的:(日常踩坑,Go,golang,slice)