概要
讲一下go中的切片,以及切片的传递
代码示例
对切片的一个理解
arr := [5]int{}
fmt.Println(len(arr)) //5
fmt.Println(cap(arr)) //5
as := arr[1:3] //含头不含尾 包含1不包含3即1,2
fmt.Println(len(as)) //2 长度指的是切片这个窗口的大小即[1:3]=>1,2
fmt.Println(cap(as)) //4 容量指的是切片可以操作的最大空间,定义的窗口是1:3,
// 窗口可以不断扩大,在底层数组不变的基础上,最大的窗口是1:len(arr)
// 你就理解你买了个石头,从中间位置给石头擦了个窗,前面的你不能擦了,后面的你可以继续擦,同一块石头
// 擦到石头末尾就不能再擦了,再擦就擦到手了
fmt.Println(&as[0]) //0x140005e2f38 切片这个窗口的第一个元素的地址,切片的第一个元素对应数组就是arr[1]
fmt.Println(&arr[1]) //0x140005e2f38
//append下面再讲,此处先忽略
as = append(as, 666, 666, 666)
fmt.Println(len(as)) //5 之前切片映射的是1跟2下标,在这个基础上添加了三个666的元素,所以长度为5 此时切片的值为 0,0,666,666,666
fmt.Println(cap(as)) //8 发现扩容了,扩容的规则百度一下吧,一个1024内2倍 1024外1.25倍 大概是这样
fmt.Println(&as[0]) //0x1400061abc0 印证了确是发生了扩容,之前的地址是0x140005e2f38,上面注释有打印说明哈
ints := make([]int, 0, 10) //make出来的切片的使用规则完全跟上面手动指定的方式是一样的
fmt.Println(len(ints))
fmt.Println(cap(ints))
fmt.Println(&ints[0]) //唯一不同的是这个地方,有些博客说make底层会搞一个默认的数组,既然有默认的数组为什么这行代码会panic呢
//我认为在make的时候必须指定长度>0,那么底层才会创建一个数组
在使用中,对append的理解
arr := [5]int{}
fmt.Println(arr) //0.0.0.0.0
as := arr[1:3]
fmt.Println(as) //0.0 对应的是数组arr[1]跟arr[2]
_ = append(as, 123) //有意思的是这个,为什么一定要 as = append 呢? 为什么 _ = append 切片看起来没变,数组却变了呢
_ = append(as, 234) //而且为什么123没有插入成功呢
fmt.Println(arr) //0.0.0.234.0
fmt.Println(as) //0.0
//我理解append的作用就是更新,或者说刷新,而且是作用于当前切片(slice struct)的更新或者是刷新,因为切片底层的struct中维护了指向数组的指针,
//切片的长度跟容量,在案例中,我们的切片长度是2容量是4,那么在这个基础上的append 123,相当于go会根据你当前的长度2并根据指向数组的指针ptr,
//进行append 123那不就相当于做了一个这样的操作吗:arr[1+2] = 123(切片是从数组的1号下标开始的,切片的长度为2),那么做完了这样的一个操作,
//是不是需要维护一下我们slice struct中的len字段呢?当然需要!所以你必须把append的结果赋值给as,至于append会创建一个slice struct
//还是刷新旧的slice struct我们不用在意,只需要知道,你必须让append的结果刷回去(我个人更倾向于是创建了一个新的struct 因为这本身并没有什么开销,
//一个struct中就ptr len cap三个字段,只是为了好理解,更好的形容append做了什么,理解为刷新也是可以的)
//那么回到我们的例子,为什么123没有插入?其实是插入了,只是被234覆盖了罢了
//为什么123 234都插入了同一个位置?那是因为我们说过,append是对当前的切片做的一个操作,当前的切片进行append可不就是把数组的3号下标的元素修改了吗,
//而且第一次123的修改,你没有及时的刷新切片,导致切片的len字段还是2,那么234的插入可不就还是插入到了3号下标位
如何不刷新的基础上append多个元素?
arr := [5]int{}
fmt.Println(arr) //0.0.0.0.0
as := arr[1:3]
fmt.Println(as) //0.0
_ = append(as, 123, 234) //在不刷新的情况下,想要批量插入多个值,可以跟上多个要插入的值
fmt.Println(arr) //0.0.0.123.234 数组的值改了,因为append就是去改你的数组的值的,改完了数组的值之后才是slice刷新
fmt.Println(as) //0.0 同样不刷新的话还是感知不到,因为slice struct的len跟cap没有变,
// 那么可以理解窗口的大小没有变,那么值怎么可能会变呢(数组的值变了,只是你看不到,因为窗口就那么大,你又没有透视眼,对吧)
我把上面这个例子变化一下,大家再猜猜是多少
arr := [5]int{}
fmt.Println(arr) //0.0.0.0.0
as := arr[1:3]
fmt.Println(as) //0.0
_ = append(as, 123, 234, 345)
fmt.Println(arr) //0.0.0.0.0 为什么会是0呢,其实很简单,数组的长度跟容量就是5,那么你操作arr[5]会报错对吧,下标越界
//同样切片这个窗口对应的位置是数组的1号跟2号下标,在这个基础上你添加了三个元素,即数组的1号位0 2号位0 3号位123 4号位234 5号位345
//但是5号位就已经下标越界了啊,所以呢,append其实会给你另创建一个底层数组,并把原始数组的内容拷贝过去
fmt.Println(as) //0.0
那么创建出来的新的数组怎么看呢?我怎么知道它确实是创建了呢,验证一下
arr := [5]int{}
fmt.Println(arr) //0.0.0.0.0
as := arr[1:3]
fmt.Println(as) //0.0
fmt.Println(&as[0]) //0x140003c6368
as = append(as, 123, 234, 345) //刷一下,或者你重新赋值给其他的变量引用都是OK的
fmt.Println(&as[0]) //0x140001e0140 确实是变了上面的数组地址是:0x140003c6368
fmt.Println(arr) //0.0.0.0.0 因为重新搞了一个数组,所以上面的append操作应用在了新的数组上arr没有变
fmt.Println(as) //[0 0 123 234 345] 底层的数组已经变了
再来聊一下切片在go的函数中的传递
func main() {
arr := [5]int{}
as := arr[1:3]
M1(as)
fmt.Println(as) //0.0
//go中的参数传递全是值拷贝
//main中的as变量在传递到M1方法中,就是做了一次拷贝
//你可以理解,as跟c是两个struct,只是这两个struct的len,cap,ptr都相同
//因为ptr相同,所以操作的是同一个数组
//从这个角度来看,其实数组的值是变了的
fmt.Println(arr[3]) // 123 确实是变了
//但是为什么切片还是0.0呢?
//那是因为在M1中的切片跟main中的切片不是同一个,所以你刷新了M1中的切片
//跟main中的切片有什么关系呢
}
func M1(c []int) {
c = append(c, 123) //刷新了切片,为什么main中的打印还是0.0
}
那么怎么解决呢?我就想传递给函数一个切片,函数内切片的改动,函数外可以感知到,那么就用切片嘛,上代码:
func main() {
arr := [5]int{}
as := arr[1:3]
as2 := as
M2(&as) //解决M2内外struct会拷贝的问题,换成切片指针,内外操作同一个切片就可以了
fmt.Println(as) //0.0.123
fmt.Println(as2) //0.0 函数内的赋值操作也是一次拷贝,as拷贝了一份给了as2 所以as2的切片还是旧的状态
fmt.Println(arr[3]) //123 同样是修改了的(大家可以试试在M2内多新增几个元素,
// 让append帮你扩容,帮你去创建一个新的数组,再来打印一下arr[3]看看是个什么结果,换汤不换药,我们上文都解释过了)
}
func M2(c *[]int) {
*c = append(*c, 123)
}