Go Slice介绍
go语言中的切片对标于其他编程语言中通俗意义上的“数组”. 切片中的元素存放在一块内存地址连续的区域,使用索引可以快速检索到指定位置的元素;切片长度和容量是可变的,在使用过程中可以根据需要进行扩容.
array:指向了内存空间地址的起点. 由于slice数据存放在连续的内存空间中,后续可以根据索引index,在起点的基础上快速进行地址偏移,从而定位到目标元素;len:切片的长度,指的是逻辑意义上slice中实际存放了多少个元素;cap:切片的容量,指的是物理意义上为slice分配了足够用于存放多少个元素的空间. 使用slice时,要求cap永远大于等于len;type slice struct { // 指向起点的地址 array unsafe.Pointer // 切片长度 len int // 切片容量 cap int }初始化:声明但不初始化,方式为
var s []int,make初始化slice也分为两种方式:s := make([]int,8),此时会将切片的长度len和容量cap同时设置为8;- 第二种方式,是分别指定切片的长度
len和容量cap,s := make([]int,8,16); - 初始化连带赋值,
s := []int{2,3,4},会将slice长度len和容量cap均设置为3,同时完成对这3个元素赋值;
slice操作,进行slice内容的截取,形如s[a:b]的格式,其中a b代表切片的索引index,左闭右开;
在对切片slice执行截取操作时,本质上是一次引用传递操作,因为不论如何截取,底层复用的都是同一块内存空间中的数据,只不过,截取动作会创建出一个新的slice header实例.
切片扩容, 当slice当前的长度len与容量cap相等时,下一次append操作就会引发一次切片扩容.
- 倘若预期的新容量超过老容量的两倍,则直接采用预期的新容量;
- 倘若老容量小于
256,则直接采用老容量的2倍作为新容量; - 倘若老容量已经大于等于
256,则在老容量的基础上扩容1/4的比例并且累加上192的数值;
slice的问题:
- 初始化切片
s长度和容量均为10在s的基础上追加append一个元素,切片s的内容、长度以及容量分别是什么?func Test_slice(t *testing.T){ s := make([]int,10) s = append(s,10) t.Logf("s: %v, len of s: %d, cap of s: %d",s,len(s),cap(s)) }输出,
s: [0 0 0 0 0 0 0 0 0 0 10], len of s: 11, cap of s: 20,make([]int, 10)其len和容量cap均为10,且前10个元素是已经切实被分配过的(零值)。在切片原容量小于256的情况下,扩容时会采用原容量的2倍作为新的容量,于是在新切片中,长度增加为11,而容量则翻倍变成20. - 初始化切片
s长度为0,容量为10,在s的基础上追加append一个元素,切片s的内容、长度以及容量分别是什么?func Test_slice(t *testing.T){ s := make([]int,0,10) s = append(s,10) t.Logf("s: %v, len of s: %d, cap of s: %d",s,len(s),cap(s)) }输出,
slice_test.go:15: s: [10], len of s: 1, cap of s: 10,make([]int, 0, 10)的方式使得切片长度0,容量为10,实际上还有长度为10的缓存空间,append的时,切片长度从0增加为1,容量则维持为10不变。 - 初始化切片
s长度为10,容量为11,在s的基础上追加append一个元素,切片s`的内容、长度以及容量分别是什么?func Test_slice(t *testing.T){ s := make([]int,10,11) s = append(s,10) t.Logf("s: %v, len of s: %d, cap of s: %d",s,len(s),cap(s)) }输出,
slice_test.go:22: s: [0 0 0 0 0 0 0 0 0 0 10], len of s: 11, cap of s: 11,由于容量大于长度,因此仍有足够的空间,这次append操作不会引发扩容。 - 初始化切片
s长度为10,容量为12,截取切片s index = 8往后的内容赋给s1,长度以及容量分别是什么?func Test_slice(t *testing.T){ s := make([]int,10,12) s1 := s[8:] t.Logf("s1: %v, len of s1: %d, cap of s1: %d",s1,len(s1),cap(s1)) }输出,
slice_test.go:29: s1: [0 0], len of s1: 2, cap of s1: 4,截取操作会以s[8]作为内存空间的起点,截取所得新切片s1的长度和容量强依赖于原切片s的长度和容量,并在此基础上减去头部8个未使用到的单位。 s长度为10,容量为12,截取切片s index为[8,9)范围内的元素赋给切片s1,长度以及容量分别是什么?func Test_slice(t *testing.T){ s := make([]int,10,12) s1 := s[8:9] t.Logf("s1: %v, len of s1: %d, cap of s1: %d",s1,len(s1),cap(s1)) }输出
slice_test.go:36: s1: [0], len of s1: 1, cap of s1: 4,我们需要注意虽然s[8:9]的截取操作限定了s1的右边界,但这只是长度意义上的,对于容量,s1仍然和s保持强关联性。- 初始化切片
s长度为10,容量为12,截取切片s index = 8往后的内容赋给s1,修改s1[0]的值,修改是否会影响到s? 此时s的内容是什么?func Test_slice(t *testing.T){ s := make([]int,10,12) s1 := s[8:] s1[0] = -1 t.Logf("s: %v",s) }输出
slice_test.go:44: s: [0 0 0 0 0 0 0 0 -1 0],s1是在s基础上截取得到的,属于一次引用传递,底层共用同一片内存空间,其中s[x]等价于s1[x+8]. 因此修改了s1[0]会直接影响到s[8]。 - 初始化切片
s长度为10,容量为12,请问,访问s[10]是否会越界?func Test_slice(t *testing.T){ s := make([]int,10,12) s = s[10] // 求问,此时数组访问是否会越界 }会发生
panic,切片长度为10,容量为12. 容量是物理意义上的,但长度是逻辑意义上的,判断是否越界以逻辑意义为准,因此index = 10已经越界。 - 初始化切片
s长度为10,容量为12,截取s中index = 8后面的内容赋给s1,在s1的基础上追加[]int{10,11,12}3个元素,经过上述操作时候,访问s[10]是否会越界?func Test_slice(t *testing.T){ s := make([]int,10,12) s1 := s[8:] s1 = append(s1,[]int{10,11,12}...) v := s[10] // ... // 求问,此时数组访问是否会越界 }会发生
panic,在s的基础上截取产生了s1,此时s1和s会拥有两个独立的slice header。s1扩容后,会被迁移到新的空间地址,此时s1已经和s做到真正意义上的完全独立,意味着修改s1不再会影响到ss继续维持原本的长度值10和容量值12,因此访问s[10]会panic。 - 初始化切片
s长度为10,容量为12,截取切片s index = 8往后的内容赋给s1,在方法changeSlice中,对s1[0]进行修改,经过上述操作之后,s的内容是什么?func Test_slice(t *testing.T){ s := make([]int,10,12) s1 := s[8:] changeSlice(s1) t.Logf("s: %v",s) } func changeSlice(s1 []int){ s1[0] = -1 }输出
slice_test.go:58: s: [0 0 0 0 0 0 0 0 -1 0],切片在传递时属于引用传递,且s1[0]和s[8]指向同一个元素. 因此在局部方法中,修改了s1[0]会直接影响到s[8]的内容. - 初始化切片
s长度为10,容量为12,截取切片s index = 8往后的内容赋给s1,在方法changeSlice中,对s1进行apend追加操作,s以及s1的内容、长度和容量分别是什么? ```go func Test_slice(t *testing.T){ s := make([]int,10,12)
s1 := s[8:] changeSlice(s1) // slice_test.go:69: s: [0 0 0 0 0 0 0 0 0 0], len of s: 10, cap of s: 12 t.Logf(“s: %v, len of s: %d, cap of s: %d”,s, len(s), cap(s)) // slice_test.go:70: s1: [0 0], len of s1: 2, cap of s1: 4 t.Logf(“s1: %v, len of s1: %d, cap of s1: %d”,s1, len(s1), cap(s1)) }
func changeSlice(s1 []int){ s1 = append(s1, 10) }
虽然切片是引用传递,但是在方法调用时,传递的会是一个新的`slice header`,因此在局部方法`changeSlice`中,虽然对`s1`进行了`append`操作,但这这会在局部方法中这个独立的`slice header`中生效,不会影响到原方法`Test_slice`当中的`s`和`s1`的长度和容量。
11. 初始化切片`s`,内容为`[]int{0,1,2,3,4}`,截取`s`中`index = 2`前面的内容(不含`s[2]`),并在此基础上追加`index = 3`后面的内容,经过上述操作后,`s`的内容、长度和内容分别是什么?此时访问`s[4]`是否会越界?
```go
func Test_slice(t *testing.T){
s := []int{0,1,2,3,4}
s = append(s[:2],s[3:]...)
// slice_test.go:82: s: [0 1 3 4], len: 4, cap: 5
t.Logf("s: %v, len: %d, cap: %d", s, len(s), cap(s))
v := s[4]
// 是否会数组访问越界
}
会panic,执行完上述append操作之后,s的实际长度为4,容量维持不变为5. 此时访问s[4]会发生数组越界的错误。
- 初始化切片
s长度和容量均为512,在s的基础上追加append一个元素,切片s的内容、长度以及容量分别是什么?func Test_slice(t *testing.T){ s := make([]int,512) s = append(s,1) // slice_test.go:91: len of s: 513, cap of s: 848 t.Logf("len of s: %d, cap of s: %d",len(s),cap(s)) }由于切片
s原有容量为512,已经超过了阈值256,因此对其进行扩容操作会采用的计算共识为512 * (512 + 3*256)/4 = 832。再终,在mallocgc流程中,我们为扩容后的新切片分配到了6784 byte的空间,于是扩容后实际的新容量为cap = 6784/8 = 848.