1. 特性:

  • 关于扩容
    1. 当 cap < 1024 时,每次 *2;
    2. 当 cap >= 1024 时,每次 *1.25;
  • 预先分配内存可以提升性能,直接使用 index 赋值而非 append 也可以提升性能:
    1. 声明时不预先指定 len 和 cap,直接用append()添加(最慢)
    2. 声明为:make([]int, 0, CAP),即指定容量,不指定长度
    3. 声明时即指定长度,也指定容量,使用下标赋值(最快)
  • 关于 slice 的底层用结构体实现,包含:
    1. 一个指向数据实际存放地址的指针
    2. 一个变量 len 代表slice的长度
    3. 一个变量 cap 代表slice的容量
      1. type slice struct {
      2. array unsafe.Pointer // 指针
      3. len int // 长度
      4. cap int // 容量
      5. }

2. 细节问题

2.1 首次扩容问题

Case 1:

  1. //2.1.1
  2. func main() {
  3. var a []int
  4. for i:=1;i<=3;i++ {
  5. a = append(a, i)
  6. }
  7. 或者 //--------------------
  8. a = append(a, 1)
  9. a = append(a, 2, 3)
  10. //--------------------
  11. fmt.Println(len(a), cap(a))
  12. 打印:3 4
  13. }

Case 2:

  1. //2.1.2
  2. func main() {
  3. var a []int
  4. a = append(a,1,2,3)
  5. 或者 //--------------------
  6. var a = []int{1, 2, 3}
  7. //--------------------
  8. fmt.Println(len(a), cap(a))
  9. 打印:3 3
  10. }

因为一开始没有指定slice的容量和大小,初始cap = 0;

  • 当首次只增加一个元素时,new_cap = 1,在后续扩容就 * 2(cap < 1024);(其实也是因为策略的第9、10行)
  • 而当第一次直接增加多个元素时,0*2 = 0,new_cap = 所需cap,所以增加3个,所需cap = 3。

扩容策略的源码:

  1. // go1.15.6 源码 src/runtime/slice.go
  2. func growslice(et *_type, old slice, cap int) slice {
  3. //省略部分判断代码
  4. //计算扩容部分
  5. //其中,cap : 所需容量,newcap : 最终申请容量
  6. newcap := old.cap
  7. doublecap := newcap + newcap
  8. if cap > doublecap { //当所需cap大于当前cap*2
  9. newcap = cap //直接申请cap = 所需的cap大小
  10. } else {
  11. if old.len < 1024 {
  12. newcap = doublecap
  13. } else {
  14. // Check 0 < newcap to detect overflow
  15. // and prevent an infinite loop.
  16. for 0 < newcap && newcap < cap {
  17. newcap += newcap / 4
  18. }
  19. // Set newcap to the requested cap when
  20. // the newcap calculation overflowed.
  21. if newcap <= 0 {
  22. newcap = cap
  23. }
  24. }
  25. }
  26. //省略部分判断代码
  27. }

2.2 传递slice在函数中的使用

  1. //2.2.1
  2. package main
  3. import "fmt"
  4. func modify(a []int) {
  5. a[0] = 1024 //尝试修改a[0]
  6. }
  7. func main() {
  8. var a = []int{1,2,3}
  9. modify(a)
  10. fmt.Println(a[0])
  11. }

输出为:1024
why?要明确:

  • Go语言中传参是值传递!
  • 上面说过,slice的第一个参数是指向数据存放地址的指针!

所以这里传过去的是指针,s[0]确实被修改了!
再看一个:

  1. //2.2.2
  2. package main
  3. import "fmt"
  4. func modify(a []int) {
  5. a[0] = 1024
  6. a = append(a, 2048) //在函数里对a追加一个元素
  7. }
  8. func main() {
  9. var a = []int{1,2,3}
  10. modify(a)
  11. fmt.Println(a)
  12. }

输出为:[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}

  1. modify(a)
  2. a = append(a, 4096) //在主函数中也对a追加一个元素
  3. fmt.Println(a)
  4. for i:=0;i<4;i++ { //查看主函数中各个元素的地址
  5. fmt.Printf("the address of a[%d] is: %v\n", i, &a[i])
  6. }

}

  1. 输出:

[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

  1. 奇怪的事情发生了——为什么a[0]的地址不一样?传过去的不是指针吗?a[0]地址不一样那是怎么修改的a[0]的值的呢?<br />事实是,这里**地址不一致的问题是由扩容引起**的(第15行,对a的声明方式)。由于声明时 cap = 3(上面已经陈述过原因),在main中进行append时进行了扩容,地址发生了变化。<br />**那我们防止扩容再看看:**
  2. ```go
  3. //2.2.4
  4. package main
  5. import "fmt"
  6. var ptr *int //声明一个指针,用来记录子函数中a[3]的地址
  7. func modify(a []int) {
  8. a[0] = 1024
  9. a = append(a, 2048)
  10. ptr = &a[3] //记录地址
  11. for i:=0;i<4;i++ {
  12. fmt.Printf("the address of a[%d] is: %v\n", i, &a[i])
  13. }
  14. }
  15. func main() {
  16. var a = make([]int, 0, 4) //预先分配容量,防止扩容
  17. for i:=1;i<=3;i++ { //初始化
  18. a = append(a, i)
  19. }
  20. modify(a)
  21. fmt.Println(a)
  22. fmt.Println(*ptr)
  23. a = append(a, 4096)
  24. //fmt.Println(cap(a))
  25. fmt.Println(a)
  26. fmt.Println(a[3])
  27. for i:=0;i<4;i++ {
  28. fmt.Printf("the address of a[%d] is: %v\n", i, &a[i])
  29. }
  30. }

输出:

  1. the address of a[0] is: 0xc000070000
  2. the address of a[1] is: 0xc000070008
  3. the address of a[2] is: 0xc000070010
  4. the address of a[3] is: 0xc000070018
  5. [1024 2 3]
  6. 2048
  7. [1024 2 3 4096]
  8. 4096
  9. the address of a[0] is: 0xc000070000
  10. the address of a[1] is: 0xc000070008
  11. the address of a[2] is: 0xc000070010
  12. 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}

  1. modify(a)
  2. fmt.Println(a)
  3. fmt.Println(cap(a)) //查看主函数中a的容量
  4. for i:=0;i<3;i++ { //查看主函数中各个元素的地址
  5. fmt.Printf("the address of a[%d] is: %v\n", i, &a[i])
  6. }

}

  1. 输出:

[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. 补充

关于slice的一些分析 - 图1