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.1
func main() {
var a []int
for 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.2
func main() {
var a []int
a = 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.go
func growslice(et *_type, old slice, cap int) slice {
//省略部分判断代码
//计算扩容部分
//其中,cap : 所需容量,newcap : 最终申请容量
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap { //当所需cap大于当前cap*2
newcap = 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.1
package main
import "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.2
package main
import "fmt"
func modify(a []int) {
a[0] = 1024
a = 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.4
package main
import "fmt"
var ptr *int //声明一个指针,用来记录子函数中a[3]的地址
func modify(a []int) {
a[0] = 1024
a = 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: 0xc000070000
the address of a[1] is: 0xc000070008
the address of a[2] is: 0xc000070010
the address of a[3] is: 0xc000070018
[1024 2 3]
2048
[1024 2 3 4096]
4096
the address of a[0] is: 0xc000070000
the address of a[1] is: 0xc000070008
the address of a[2] is: 0xc000070010
the 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切片已经指向别的地址了)。