1. 特性:
- 关于扩容
- 当 cap < 1024 时,每次 *2;
- 当 cap >= 1024 时,每次 *1.25;
- 预先分配内存可以提升性能,直接使用 index 赋值而非 append 也可以提升性能:
- 声明时不预先指定 len 和 cap,直接用append()添加(最慢)
- 声明为:make([]int, 0, CAP),即指定容量,不指定长度
- 声明时即指定长度,也指定容量,使用下标赋值(最快)
- 关于 slice 的底层用结构体实现,包含:
- 一个指向数据实际存放地址的指针
- 一个变量 len 代表slice的长度
- 一个变量 cap 代表slice的容量
type slice struct {array unsafe.Pointer // 指针len int // 长度cap int // 容量}
2. 细节问题
2.1 首次扩容问题
Case 1:
//2.1.1func main() {var a []intfor i:=1;i<=3;i++ {a = append(a, i)}或者 //--------------------a = append(a, 1)a = append(a, 2, 3)//--------------------fmt.Println(len(a), cap(a))打印:3 4}
Case 2:
//2.1.2func main() {var a []inta = append(a,1,2,3)或者 //--------------------var a = []int{1, 2, 3}//--------------------fmt.Println(len(a), cap(a))打印:3 3}
因为一开始没有指定slice的容量和大小,初始cap = 0;
- 当首次只增加一个元素时,new_cap = 1,在后续扩容就 * 2(cap < 1024);(其实也是因为策略的第9、10行)
- 而当第一次直接增加多个元素时,0*2 = 0,new_cap = 所需cap,所以增加3个,所需cap = 3。
扩容策略的源码:
// go1.15.6 源码 src/runtime/slice.gofunc growslice(et *_type, old slice, cap int) slice {//省略部分判断代码//计算扩容部分//其中,cap : 所需容量,newcap : 最终申请容量newcap := old.capdoublecap := newcap + newcapif cap > doublecap { //当所需cap大于当前cap*2newcap = cap //直接申请cap = 所需的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}}}//省略部分判断代码}
2.2 传递slice在函数中的使用
//2.2.1package mainimport "fmt"func modify(a []int) {a[0] = 1024 //尝试修改a[0]}func main() {var a = []int{1,2,3}modify(a)fmt.Println(a[0])}
输出为:1024
why?要明确:
- Go语言中传参是值传递!
- 上面说过,slice的第一个参数是指向数据存放地址的指针!
所以这里传过去的是指针,s[0]确实被修改了!
再看一个:
//2.2.2package mainimport "fmt"func modify(a []int) {a[0] = 1024a = append(a, 2048) //在函数里对a追加一个元素}func main() {var a = []int{1,2,3}modify(a)fmt.Println(a)}
输出为:[1024 2 3]
why?2048 呢?
- Go语言中传参是值传递!
- 传过去的是指针,修改值没问题;但是len和cap也都是值传递!
也就是说,主函数main里的切片a,其cap(a)仍然为3。
那么问题来了:
- 既然是同一个指针,即使main里的cap(a)=3,那 a 所指向的地址的第四个元素(“a[3]”)到底是不是2048呢?
- 既然在子函数中对a所指向的地址的“第4个位置”赋了值,那么再在主函数main中对a追加一个元素,进行的操作是覆盖还是跳过呢? ```go //2.2.3 package main
import “fmt”
func modify(a []int) { a[0] = 1024 a = append(a, 2048) //在函数里对a追加一个元素 fmt.Println(a) for i:=0;i<4;i++ { //查看子函数中a各个元素的地址 fmt.Printf(“the address of a[%d] is: %v\n”, i, &a[i]) } }
func main() { var a = []int{1,2,3}
modify(a)a = append(a, 4096) //在主函数中也对a追加一个元素fmt.Println(a)for i:=0;i<4;i++ { //查看主函数中各个元素的地址fmt.Printf("the address of a[%d] is: %v\n", i, &a[i])}
}
输出:
[1024 2 3 2048] the address of a[0] is: 0xc00006c030 the address of a[1] is: 0xc00006c038 the address of a[2] is: 0xc00006c040 the address of a[3] is: 0xc00006c048 [1024 2 3 4096] the address of a[0] is: 0xc00006c060 the address of a[1] is: 0xc00006c068 the address of a[2] is: 0xc00006c070 the address of a[3] is: 0xc00006c078
奇怪的事情发生了——为什么a[0]的地址不一样?传过去的不是指针吗?a[0]地址不一样那是怎么修改的a[0]的值的呢?<br />事实是,这里**地址不一致的问题是由扩容引起**的(第15行,对a的声明方式)。由于声明时 cap = 3(上面已经陈述过原因),在main中进行append时进行了扩容,地址发生了变化。<br />**那我们防止扩容再看看:**```go//2.2.4package mainimport "fmt"var ptr *int //声明一个指针,用来记录子函数中a[3]的地址func modify(a []int) {a[0] = 1024a = append(a, 2048)ptr = &a[3] //记录地址for i:=0;i<4;i++ {fmt.Printf("the address of a[%d] is: %v\n", i, &a[i])}}func main() {var a = make([]int, 0, 4) //预先分配容量,防止扩容for i:=1;i<=3;i++ { //初始化a = append(a, i)}modify(a)fmt.Println(a)fmt.Println(*ptr)a = append(a, 4096)//fmt.Println(cap(a))fmt.Println(a)fmt.Println(a[3])for i:=0;i<4;i++ {fmt.Printf("the address of a[%d] is: %v\n", i, &a[i])}}
输出:
the address of a[0] is: 0xc000070000the address of a[1] is: 0xc000070008the address of a[2] is: 0xc000070010the address of a[3] is: 0xc000070018[1024 2 3]2048[1024 2 3 4096]4096the address of a[0] is: 0xc000070000the address of a[1] is: 0xc000070008the address of a[2] is: 0xc000070010the address of a[3] is: 0xc000070018
从打印的结果我们可以看出:
- 子函数进行append时,a所指向的地址的第4个元素确实为2048,只是因为”cap”是main值传给modify的,所以main中并未记录”a[3] = 2048”;
在main中再进行append后,进行的操作是覆盖,可以发现,同一地址的“2048”变成“4096”了。
2.3 关于扩容可能会造成的问题
再看看代码2.2.3,我们注意到子函数和主函数中s的地址由于扩容已经不一样了,这会有几个思考:
是否有丢失修改的风险?
- 是都扩容了还是哪边需要哪边扩容? ```go package main
import “fmt”
func modify(a []int) {
a[1] = 0 //扩容前的修改
a = append(a, 2048)
a = append(a, 4096) //在函数里对a追加两个元素,引起扩容
a[0] = 1024 //扩容后进行修改
fmt.Println(a)
fmt.Println(cap(a)) //查看子函数中a的容量
for i:=0;i<4;i++ { //查看子函数中a各个元素的地址
fmt.Printf(“the address of a[%d] is: %v\n”, i, &a[i])
}
}
func main() { var a = []int{1,2,3}
modify(a)fmt.Println(a)fmt.Println(cap(a)) //查看主函数中a的容量for i:=0;i<3;i++ { //查看主函数中各个元素的地址fmt.Printf("the address of a[%d] is: %v\n", i, &a[i])}
}
输出:
[1024 0 3 2048 4096] 6 the address of a[0] is: 0xc00006c030 the address of a[1] is: 0xc00006c038 the address of a[2] is: 0xc00006c040 the address of a[3] is: 0xc00006c048 [1 0 3] 3 the address of a[0] is: 0xc000070000 the address of a[1] is: 0xc000070008 the address of a[2] is: 0xc000070010 ``` 可以发现:
- 哪边需要扩容,哪边才扩容;
- 扩容前的修改保留,扩容后的修改丢失(扩容前a切片在子/主函数中都指向同一地址;扩容是开辟一片新的地址,然后将数据复制过去,所以扩容后子函数中的a切片已经指向别的地址了)。
3. 补充

