type slice struct {
array unsafe.Pointer
len int
cap int
}
1.array: 是一个非安全类型的指针,指向底层数组,是一个连续的内存块。
2.len: 指slice的实际长度,既slice中含有的元素个数。
3.cap: 指slice的底层数组长度,即slice的容量,slice可以容纳多少元素。
tips:底层数组可以被多个slice同时指向,因此对一个slice的元素进行操作,有可能会影响到其他切片。
1.使用“截取” 的方法:
截取是常见的一种创建slice的方法,可以从数组和slice直接截取,需要指定起始位置。
值得注意的是,截取之后的slice,和被截取的slice共享同一个底层数组,新老slice对底层数组的更改都会影响到彼此。所以,问题的关键在于是否共享同一个底层数组。
2.使用make方法定义切片:
//创建一个长度为3,容量为4的int类型切片
slice := make([]int, 3, 4)
如果后面的长度和容量只填一个,那么会默认指定该切片的长度和容量是相等的。
切片带有自动扩容机制,一般是在使用了append函数向切片追加了元素之后,切片的容量不足,引起了扩容。那么扩容机制的规律是什么呢,让我们继续往下看。
通过查询官网资料,得到了一个结论,当切片进行自动扩容时,会调用growSlice函数,让我们看下这个函数的源码。
func growslice(et *_type, old slice, cap int) slice {
// ……
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
const threshold = 256
if old.cap < threshold {
newcap = doublecap
} else {
for 0 < newcap && newcap < cap {
// Transition from growing 2x for small slices
// to growing 1.25x for large slices. This formula
// gives a smooth-ish transition between the two.
newcap += (newcap + 3*threshold) / 4
}
if newcap <= 0 {
newcap = cap
}
}
// ……
capmem = roundupsize(uintptr(newcap) * ptrSize)
newcap = int(capmem / ptrSize)
该函数的主要逻辑是根据当前切片的容量和需要扩展到的容量计算出新的容量newcap。在计算新的容量时,函数采用了一种渐进式的扩容策略,即当切片的容量小于一个阈值threshold(256)时,每次扩容将容量翻倍;当切片的容量大于等于threshold时,每次扩容将容量增加原来容量的 1.25 倍。
在计算出新的容量后,函数根据新的容量计算出需要分配的内存大小capmem,并通过roundupsize函数进行内存对齐。最后,函数将capmem转换为新的容量newcap,并返回新的切片。
前面我们说到,slice其实是一个结构体,当 slice 作为函数参数时,就是一个普通的结构体。若直接传slice,在调用者看来,实参 slice 并不会被函数中的操作改变;若传的是 slice 的指针,在调用者看来,是会被改变原 slice 的。
需要注意的是,不管传的是 slice 还是 slice 指针,如果改变了 slice 底层数组的数据,会反应到实参 slice 的底层数据。
那么问题来了,为什么能改变底层数组的数据?很好理解:底层数据在 slice 结构体里是一个指针,尽管 slice 结构体自身不会被改变,也就是说底层数据地址不会被改变。但是通过指向底层数据的指针,可以改变切片的底层数据。通过 slice 的 array 字段就可以拿到数组的地址。另外在Go语言里,函数参数传递,只有值传递,没有引用传递。
接下来让我们看几个例题。
1.请问下面代码输出的是什么?
func main() {
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := slice[2:5]
s2 := s1[2:6:7]
s2 = append(s2, 100)
s2 = append(s2, 200)
s1[2] = 20
fmt.Println(s1)
fmt.Println(s2)
fmt.Println(slice)
}
输出结果为:
首先s1是通过从索引2到索引5(不包括 5)对原始 slice进行切片创建的。这意味着s1将包含元素 [2, 3, 4]。
s2 是通过从s1的索引2到索引6(不包括 6),并且其容量为7(不包含7)进行切片创建的。在slice表达式中的容量参数指定了在切片后可以访问的底层数组的最大长度。在这种情况下,s2的容量为7,这意味着s2的底层数组长度为7,即s2只包含元素 [4,5,6,7]。
接着向s2追加一个元素100,因为s2容量刚好够,直接追加。不过,这会修改原始数组对应位置的元素。这一改动,数组和 s1 都同步进行了变化。
接着再次向S2追加元素200,此时,s2的容量不够用,进行自动扩容了。于是,s2就另起炉灶了,将原来的元素复制到了新的位置,扩大自己的容量。
最后,修改s1索引为2位置的元素,s1[2] = 20。但是只会影响原始数组相应位置的,元素了,不会影响s2,因为人家已经跑路了。最后结果如上图所示。
让我们来看最后一个例子,请问这段代码最后输出的是什么?
package main
import "fmt"
func myAppend(s []int) []int {
// 这里 s 虽然改变了,但并不会影响外层函数的 s
s = append(s, 100)
return s
}
func myAppendPtr(s *[]int) {
// 会改变外层 s 本身
*s = append(*s, 100)
return
}
func main() {
s := []int{1, 1, 1}
newS := myAppend(s)
fmt.Println(s)
fmt.Println(newS)
s = newS
myAppendPtr(&s)
fmt.Println(s)
}
运行结果:
myAppend 函数接收一个 []int 类型的 slice,向其中添加一个元素 100,并返回新的 slice。这个函数内部对传入的 slice 进行了修改,但是这个修改不会影响到外层函数中的 s,因为 s 是传值调用的,即函数中对 s 的操作不会影响到外部的 s。
myAppendPtr 函数接收一个 *[]int 类型的 slice 指针,向其中添加一个元素 100。这个函数内部使用了 append 函数来修改指针指向的 slice,并且这个修改会影响到外层函数中指针指向的 slice。
main 函数首先定义了一个 []int 类型的 slice s,并向其中添加了三个元素 {1, 1, 1}。然后调用 myAppend 函数来向 s 中添加一个元素 100,并将返回的新的 slice 赋值给 newS。此时,s 和 newS 分别指向两个不同的 slice,因此输出时会发现 s 中仍然只包含三个元素 {1, 1, 1},而 newS 中包含四个元素 {1, 1, 1, 100}。
接着,s 被赋值为 newS,即 s 和 newS 指向同一个 slice。然后调用 myAppendPtr 函数来向 s 中添加一个元素 100。由于 myAppendPtr 函数是按照指针传递的,所以函数内部对 s 的修改会影响到外层函数中的 s,因此输出时会发现 s 中包含了五个元素 {1, 1, 1, 100,100}。
//增加
func Add(s []int, index int, value int) []int {
//如果插入的长度超过了切片的长度,则直接在末尾插入元素
if index > len(s) {
return append(s, value)
}
//如果插入的位置在切片中间,则需要讲该位置后面的元素全部向后移
s = append(s[:index+1], s[index:len(s)-1]...)
s[index] = value
return s
}
//删除
func Delete(s []int, index int, value int) []int {
//如果删除的位置超出了切片的长度,则直接返回原切片。
if index >= len(s) {
return s
}
// 如果删除的位置在切片中间,则需要将该位置后面的元素全部向前移动一位
copy(s[index:], s[index+1:])
return s[:len(s)-1]
}