golang中数组与切片的Q&A
数组与切片 的 Q&A
切片作为函数参数
type slice struct {
array unsafe.Pointer
len int
cap int
}
- slice其实是一个结构体,包含三个成员,len、map、array,分别表示长度、容量和底层数组的地址
- 当slice作为函数参数时,就是一个普通的结构体,其实很好理解,若直接传slice,在调用者看来,实参slice并不会
被函数中的操作所改变,若传的是slice的指针,在调用者看来,是会被改变原slice的 - 值得注意的是,不管传递的是slice还是slice指针,如果改变了slice底层数组的数据,会反映到实参slice的底层数据
为什么能改变底层数组的数据?
很好理解,底层数组数据在结构体里是一个指针unsafe.Pointer类型,尽管slice传递是复制,但是它们指向底层数组的指针是一样的 - 通过slice的array字段就可以拿到数组的地址,在代码里直接通过s[i] = 10这种操作改变slice底层数组元素的值
- 另外值得注意的是,go语言的函数参数传递,只有值传递,没有引用传递
func main() {
s := []int{1, 1, 1}
fmt.Println(s)
f(s)
fmt.Println(s)
}
func f(s []int) {
// range会拷贝一份(copy方法),不会对原切片造成影响
//for _, v := range s{
// v++
//}
for i, _ := range s{
s[i] += 1 // 修改的是底层数组的值
}
}
- 果真改变了原始slice的底层数据,这里传递的是一个slice副本,在f函数中,s只是main函数中s的一个拷贝,在f函数内部对s的作用
并不会改变外层main函数中的s - 要想真的改变外层slice,只有将返回新的slice赋值到原始slice,或者向函数传递一个指向slice的指针,下面的例子
func myAppend(s []int) []int {
// append方法导致了切片的扩容,底层数组复制,所以不会影响到外部切片
s = append(s, 100)
return s
}
func myAppendPtr(s *[]int) {
// append方法导致了切片的扩容,底层数组复制,但是扩容也好,复制也巴都是针对外部切片的,因为传递进来的是指针
*s = append(*s, 1000)
}
func main() {
s := []int{1, 2, 3}
newS := myAppend(s)
fmt.Println(s)
fmt.Println(newS)
s = newS
fmt.Println(s)
myAppendPtr(&s)
fmt.Println(s)
}
- myAppend函数里虽然改变了s,但它只是一个值传递,并不会影响最外层的s,因此第一行打印[1 2 3]
而newS是一个新的slice,是基于s得到的,因此它打印的是追加了100之后的结果,[1,2,3,100]
最后newS赋值给了s,s这是才真正变成了一个新的slice,在给myAppendPtr传入一个s的指针,这回它真的改变了[1,2,3,100,1000]
切片的容量是怎样增长的?
- 一般都是向slice追加了元素,才会引起扩容,追加元素调用的是append函数,先来看看append函数的原型
func append(slice []Type, elems ...Type) []Type
- append函数的参数长度可变,因此可以追加多个值到slice中,也可以追加一个切片...
slice = append(slice, elem1, elem2)
slice = append(slice, anotherSlice...)
func main() {
s1 := []int{1, 2, 3}
// 数组的指针就是数组中第一个元素的指针
// 切片的地址 切片的值 切片指向底层数组的地址 == 切片指向底层数组第一个元素的地址
fmt.Printf("%p:%v:%p:%p\n", &s1, s1, s1, &(s1[0]))
// 切片扩容,底层数组复制,切片的地址不变,但是底层数组的地址改变了
s1 = append(s1, []int{4, 5}...)
fmt.Printf("%p:%v:%p:%p\n", &s1, s1, s1, &(s1[0]))
}
输出结果
0xc000004078:[1 2 3]:0xc000010180:0xc000010180
0xc000004078:[1 2 3 4 5]:0xc00000c360:0xc00000c360
- append()函数的返回值是一个新的slice, go编译器不允许调用了append函数后不使用返回值
append(slice, elem1, elem2)
append(slice, anotherSlice...)
所以上面这种写法是错的,编译器不能通过
4. 使用append可以向slice添加元素,实际上是向底层数组添加元素,但是底层数组的长度是固定的,如果索引
len-1所指向的元素已经是底层数组的最后一个元素,就没办法添加了
5. 切片扩容的规则
1. 当原slice的容量小于1024的时候,新slice的容量是原slice容量的2倍
2. 当原slice的容量大于1024的时候,新slice的容量是原slice容量的1.25倍
6. 其实上面说法是有误的,源码中
func growslice(et *_type, old slice, cap int) slice {
源码上半部分确实如上所说进行扩容,但是后面会进行内存对齐,这个和内存分配策略相关,内存对齐后,
新slice的容量要大于等于就slice容量的2倍或1.25倍
7. 经典例子1
func main() {
s := []int{5} // s长度1,容量1
s = append(s, 7) // s长度2,容量2
s = append(s, 9) // s长度3, 容量4
x := append(s, 11) // x长度4,容量4,s长度3,容量4 此处没有发生扩容
y := append(s, 12) // y长度4,容量4,s长度3,容量4 此处也没有发生扩容
fmt.Println(len(s), cap(s)) // 3 4
fmt.Println(len(x), cap(x)) // 4 4
fmt.Println(len(y), cap(y)) // 4 4
fmt.Println(s, x, y) // [5 7 9] [5 7 9 12] [5 7 9 12]
}
上面案例代码 切片对应状态
s := []int{5} s 只有一个元素,[5]
s = append(s, 7) s 扩容,容量变为2,[5, 7]
s = append(s, 9) s 扩容,容量变为4,[5, 7, 9]。注意,这时 s 长度是3,只有3个元素
x := append(s, 11) 由于 s 的底层数组仍然有空间,因此并不会扩容。这样,底层数组就变成了 [5, 7, 9, 11]。注意,此时 s = [5, 7, 9],容量为4;x = [5, 7, 9, 11],容量为4。这里 s 不变
y := append(s, 12) 这里还是在 s 元素的尾部追加元素,由于 s 的长度为3,容量为4,所以直接在底层数组索引为3的地方填上12。结果:s = [5, 7, 9],y = [5, 7, 9, 12],x = [5, 7, 9, 12],x,y 的长度均为4,容量也均为4
8. 经典例子2
在看一个例子,跟上面的有一点不太一样
func main() {
s := []int{5} // s长度1,容量1
s = append(s, 7) // s长度2,容量2
s = append(s, 9) // s长度3, 容量4
x := append(s, 11) // x长度4,容量4,s长度3,容量4
y := append(x, 12) // y长度5,容量8,s长度3,容量4
fmt.Println(len(s), cap(s)) // 3 4
fmt.Println(len(x), cap(x)) // 4 4
fmt.Println(len(y), cap(y)) // 5 8
fmt.Println(s, x, y) // [5 7 9] [5 7 9 11] [5 7 9 11 12]
}
- 总结:这里需要注意的是,append函数返回的是全新的slice, 并且对传入的slice不会有影响
- 再看一个例子:
func main() {
s := []int{11, 22}
s = append(s, 33, 44, 55)
fmt.Println(s, len(s), cap(s)) // [11 22 33 44 55] 5 6
}
看一段源码
// go 1.9.5 src/runtime/slice.go:82
func growslice(et *_type, old slice, cap int) slice {
// ……
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
// ……
}
// ……
capmem = roundupsize(uintptr(newcap) * ptrSize)
newcap = int(capmem / ptrSize)
}
我们可以看到,如果求的的新的容量cap>2倍的旧容量,那么newcap直接等于新求的容量cap
由于后面有内存对齐相关的源码,最终容量变成了6,:
参考文档: https://www.bookstack.cn/read/qcrao-Go-Questions/数组和切片-切片的容量是怎样增长的.md
10. 向一个nil的slice添加元素会发生什么,为什么?
其实nil slice或者empty slice都可以通过append函数实现底层数组的扩容,最终都是调用mallocgc向go的内存管理器申请一块内存,
然后赋值给原来的nil slice或emtpy slice,然后摇身一变,变成了真正的"slice"了
数组和切片有什么异同
- slice的底层数据是数组,slice是对底层数组的封装,它描述一个数组的片段,两者都可以通过下标访问单个元素
- 数组是定长的,长度定义好之后,不能在更改,在go中,数组是不常见的,因为它的长度是类型的一部分,[3]int和[4]int是不同的类型
- 而切片非常的灵活,他可以动态扩容,而且它的类型和长度无关
- 数组就是一片连续的内存,slice实际上是一个结构体,包含三个字段array数组指针,len长度,cap容量
// runtime/slice.go
type slice struct {
array unsafe.Pointer // 元素指针
len int // 长度
cap int // 容量
}
- 一定要知道,数组是一块连续的内存
- 注意, 底层数组可以被多个slice同时指向的,因此对一个slice的元素进行操作,有可能影响到其它slice
【引申1】[3]int和[4]int是同一个类型吗
答:不是,因为数组的长度也是类型的一部分,这一点跟切片不太一样 - 对切片进行[:],和append元素的经典案例:
func main() {
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := slice[2:5] // s1=[2, 3, 4] len=3 cap=8
s2 := s1[2:6:7] // s2=[4, 5, 6, 7] len=4 cap=5
// 下面代码会改变底层数组的值:8->100,因为s1和slice也引用的底层数组,所以它俩都会受到影响
// 此时slice = [0, 1, 2, 3, 4, 5, 6, 7, 100, 9], 由于s1的长度为3,所以它虽然底层数组收到了影响,但是它没变化
s2 = append(s2, 100) // s2=[4, 5, 6, 7, 100] len=5 cap=5
// s2发生了扩容,底层数组复制,增加的200元素不会对s1和slice的底层数组产生影响
s2 = append(s2, 200) // s2=[4, 5, 6, 7, 100, 200] len=6 cap=10
s1[2] = 20 // s1=[2, 3, 20] len=3 cap=8, slice的4->20 slice=[0, 1, 2, 3, 20, 5, 6, 7, 100, 9]
fmt.Println(s1) // [2, 3, 20]
fmt.Println(s2) // [4, 5, 6, 7, 100, 200]
fmt.Println(slice) // [0, 1, 2, 3, 20, 5, 6, 7, 100, 9]
}