内部数据结构

@(src/runtime/slice.go)

slice 仅有三个字段,其中array 是保存数据的部分,len 字段为长度,cap 为容量。

  1. type slice struct {
  2. array unsafe.Pointer // 数据部分
  3. len int // 长度
  4. cap int // 容量
  5. }

创建

创建一个slice,其实就是分配内存。cap, len 的设置在汇编中完成。

slice源码分析 - 图1
下面的代码主要是做了容量大小的判断,以及内存的分配。
@(src/runtime/slice.go)

  1. func makeslice(et *_type, len, cap int) unsafe.Pointer {
  2. // 获取需要申请的内存大小
  3. mem, overflow := math.MulUintptr(et.size, uintptr(cap))
  4. if overflow || mem > maxAlloc || len < 0 || len > cap {
  5. mem, overflow := math.MulUintptr(et.size, uintptr(len))
  6. if overflow || mem > maxAlloc || len < 0 {
  7. panicmakeslicelen()
  8. }
  9. panicmakeslicecap()
  10. }
  11. // 分配内存
  12. // 小对象从当前P 的cache中空闲数据中分配
  13. // 大的对象 (size > 32KB) 直接从heap中分配
  14. // runtime/malloc.go
  15. return mallocgc(mem, et, true)
  16. }

append

对于不需要内存扩容的slice,直接数据拷贝即可。
注意:append是在现有的长度上追加

  1. func main() {
  2. a := make([]int,10,100)
  3. a = append(a,1)
  4. fmt.Println(a) // [0 0 0 0 0 0 0 0 0 0 1]
  5. }

slice源码分析 - 图2
上面的DX 存放的就是array 指针,AX 是数据的偏移. 将 123 存入数组。
而对于容量不够的情况,就需要对slice 进行扩容。这也是slice 比较关心的地方。 (因为对于大slice,grow slice会影响到内存的分配和执行的效率)
@(src/runtime/slice.go)

  1. func growslice(et *_type, old slice, cap int) slice {
  2. // 静态分析, 内存扫描
  3. // ...
  4. if cap < old.cap {
  5. panic(errorString("growslice: cap out of range"))
  6. }
  7. // 如果存储的类型空间为0, 比如说 []struct{}, 数据为空,长度不为空
  8. if et.size == 0 {
  9. return slice{unsafe.Pointer(&zerobase), old.len, cap}
  10. }
  11. newcap := old.cap
  12. doublecap := newcap + newcap
  13. if cap > doublecap {
  14. // 如果新容量大于原有容量的两倍,则直接按照新增容量大小申请
  15. newcap = cap
  16. } else {
  17. if old.len < 1024 {
  18. // 如果原有长度小于1024,那新容量是老容量的2倍
  19. newcap = doublecap
  20. } else {
  21. // 按照原有容量的1/4 增加,直到满足新容量的需要
  22. for 0 < newcap && newcap < cap {
  23. newcap += newcap / 4
  24. }
  25. // 通过校验newcap 大于0检查容量是否溢出。
  26. if newcap <= 0 {
  27. newcap = cap
  28. }
  29. }
  30. }
  31. var overflow bool
  32. var lenmem, newlenmem, capmem uintptr
  33. // 为了加速计算(少用除法,乘法)
  34. // 对于不同的slice元素大小,选择不同的计算方法
  35. // 获取需要申请的内存大小。
  36. switch {
  37. case et.size == 1:
  38. lenmem = uintptr(old.len)
  39. newlenmem = uintptr(cap)
  40. capmem = roundupsize(uintptr(newcap))
  41. overflow = uintptr(newcap) > maxAlloc
  42. newcap = int(capmem)
  43. case et.size == sys.PtrSize:
  44. lenmem = uintptr(old.len) * sys.PtrSize
  45. newlenmem = uintptr(cap) * sys.PtrSize
  46. capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
  47. overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
  48. newcap = int(capmem / sys.PtrSize)
  49. case isPowerOfTwo(et.size):
  50. // 二的倍数,用位移运算
  51. var shift uintptr
  52. if sys.PtrSize == 8 {
  53. // Mask shift for better code generation.
  54. shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
  55. } else {
  56. shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
  57. }
  58. lenmem = uintptr(old.len) << shift
  59. newlenmem = uintptr(cap) << shift
  60. capmem = roundupsize(uintptr(newcap) << shift)
  61. overflow = uintptr(newcap) > (maxAlloc >> shift)
  62. newcap = int(capmem >> shift)
  63. default:
  64. // 其他用除法
  65. lenmem = uintptr(old.len) * et.size
  66. newlenmem = uintptr(cap) * et.size
  67. capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
  68. capmem = roundupsize(capmem)
  69. newcap = int(capmem / et.size)
  70. }
  71. // 判断是否会溢出
  72. if overflow || capmem > maxAlloc {
  73. panic(errorString("growslice: cap out of range"))
  74. }
  75. // 内存分配
  76. var p unsafe.Pointer
  77. if et.kind&kindNoPointers != 0 {
  78. p = mallocgc(capmem, nil, false)
  79. // 清空不需要数据拷贝的部分内存
  80. memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
  81. } else {
  82. // Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
  83. p = mallocgc(capmem, et, true)
  84. if writeBarrier.enabled { // gc 相关
  85. // Only shade the pointers in old.array since we know the destination slice p
  86. // only contains nil pointers because it has been cleared during alloc.
  87. bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem)
  88. }
  89. }
  90. // 数据拷贝
  91. memmove(p, old.array, lenmem)
  92. return slice{p, old.len, newcap}
  93. }

切片拷贝 (copy)

切片的浅拷贝

直接切就是浅拷贝

  1. func main() {
  2. a := make([]int,10,100)
  3. a1 := a[9:12:12]
  4. a1[0] = 100
  5. fmt.Println(a) // [0 0 0 0 0 0 0 0 0 100]
  6. }

切片的深拷贝

深拷贝也比较简单,只是做了一次内存的深拷贝。
对于使用者角度,用copy即可

  1. func slicecopy(to, fm slice, width uintptr) int {
  2. if fm.len == 0 || to.len == 0 {
  3. return 0
  4. }
  5. n := fm.len
  6. if to.len < n {
  7. n = to.len
  8. }
  9. // 元素大小为0,则直接返回
  10. if width == 0 {
  11. return n
  12. }
  13. // 竟态分析和内存扫描
  14. // ...
  15. size := uintptr(n) * width
  16. // 直接内存拷贝
  17. if size == 1 { // common case worth about 2x to do here
  18. *(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer
  19. } else {
  20. memmove(to.array, fm.array, size)
  21. }
  22. return n
  23. }
  24. // 字符串slice的拷贝
  25. func slicestringcopy(to []byte, fm string) int {
  26. if len(fm) == 0 || len(to) == 0 {
  27. return 0
  28. }
  29. n := len(fm)
  30. if len(to) < n {
  31. n = len(to)
  32. }
  33. // 竟态分析和内存扫描
  34. // ...
  35. memmove(unsafe.Pointer(&to[0]), stringStructOf(&fm).str, uintptr(n))
  36. return n
  37. }

引用类型作为参数时传递的是地址还是值?

在 Golang 中函数之间传递变量时总是以值的方式传递的,无论是 int,string,bool,array 这样的内置类型(或者说原始的类型),还是 slice,channel,map 这样的引用类型,在函数间传递变量时,都是以值的方式传递,也就是说传递的都是值的副本

感觉值传递示例:

  1. func main() {
  2. slice := []int{1, 2, 3, 4, 5}
  3. fmt.Printf("切片:%v\n", slice)
  4. setSliceNil(slice)
  5. fmt.Printf("切片:%v\n", slice)
  6. }
  7. func setSliceNil(slice []int) {
  8. slice = nil
  9. fmt.Printf("切片:%v\n", slice)
  10. }
  11. 切片:[1 2 3 4 5]
  12. 切片:[]
  13. 切片:[1 2 3 4 5]
  14. 经过函数中对slice赋值nil并没有改变main中的slice变量的值,说明go中引用类型的传递也是值传递

感觉指针传递示例

  1. func main() {
  2. slice := []int{1, 2, 3, 4, 5}
  3. fmt.Printf("切片:%v\n", slice)
  4. changeSlice(slice)
  5. fmt.Printf("切片:%v\n", slice)
  6. }
  7. func changeSlice(slice []int) {
  8. slice[2] = 10
  9. fmt.Printf("切片:%v\n", slice)
  10. }
  11. 切片:[1 2 3 4 5]
  12. 切片:[1 2 10 4 5]
  13. 切片:[1 2 10 4 5]
  14. 这里的结果会让人产生困惑,既然是值传递为什么main函数中的切片被修改了呢

golang所有参数传递都是值传递

为什么都是值传递,会有指针传递的效果,这个得从源码的角度来说

以切片为例:定义一个切片返回的是一个指针

  1. type slice struct {
  2. array unsafe.Pointer // 数据部分
  3. len int // 长度
  4. cap int // 容量
  5. }
  6. func makeslice(et *_type, len, cap int) unsafe.Pointer {...}
  7. 注意:1.10版本之前makeslice返回的是一个slice
  8. func makeslice(et *_type, len, cap int) slice {...}
  9. 这个问题,让我纠结了很久,这里的makeslice实际上创建的是底层数组(也就是slice.array),
  10. 注意:在reflect包也解释了,SliceHeader才是slice运行的结构
  11. type SliceHeader struct {
  12. Data uintptr
  13. Len int
  14. Cap int
  15. }

我以为,map与slice传递的都是指针类型,实际上不是这样的,例如下面

  1. type a struct {
  2. a []int64
  3. }
  4. type b struct {
  5. b []int64
  6. }
  7. func main() {
  8. a := a{make([]int64, 10, 100)}
  9. a.a[0] = 1
  10. fmt.Printf("%p\n", a.a)
  11. b := b{a.a}
  12. fmt.Printf("%p\n", b.b)
  13. b.b = append(b.b, 1, 2, 3)
  14. b.b[0] = 0
  15. fmt.Printf("%p\n", b.b)
  16. fmt.Println(b.b)
  17. fmt.Println(a.a)
  18. }
  19. 0xc000070000
  20. 0xc000070000
  21. 0xc000070000
  22. [0 0 0 0 0 0 0 0 0 0 1 2 3]
  23. [0 0 0 0 0 0 0 0 0 0]

可以看到地址没变,b.b的长度增长没影响到a.a 但a.a[0] 却受到了 b.b[0]=0的影响

或下面

  1. func main() {
  2. slice := make([]int, 10, 20)
  3. fmt.Printf("%p\n", slice) // 0xc00008e000
  4. bbb(slice)
  5. fmt.Println(len(slice)) // 10
  6. }
  7. func bbb(slice []int) {
  8. fmt.Printf("%p\n", slice) // 0xc00008e000
  9. for v := 0; v < 10; v++ {
  10. slice = append(slice, v)
  11. }
  12. // 虽然这里的地址没变,但不会影响到main中的slice
  13. // 为什么append后地址没变,是因为main中的slice初始化时,容量给的较大
  14. // 这里我们在循环中,v改大一点,是会导致slice底层的数组重新分配一次内存
  15. fmt.Printf("%p\n", slice) // 0xc00008e000
  16. fmt.Println(len(slice)) //20
  17. }

问题来了,为什么地址是一样的,但是main中slice的长度没有改变,注意:fmt.Printf(“%p\n”, slice)这里打印的是底层数组的数组的标头地址,上面的append并没有导致扩容,所以底层的数组是没有变化的,而slice传递是值传递,会将len=10,cap=20,data=0xc00008e000传给方法bbb,bbb中的slice由于append,长度发生了变化,但这并不影响main中的slice的长度

结论:在参数传递中还是传递的slice结构体

切片与数组

数组是定长的,长度定义好之后,不能再更改。在 Go 中,数组是不常见的,因为其长度是类型的一部分,限制了它的表达能力,比如 [3]int 和 [4]int 就是不同的类型。
而切片则非常灵活,它可以动态地扩容。切片的类型和长度无关。
数组就是一片连续的内存, slice 实际上是一个结构体,包含三个字段:长度、容量、底层数组。
@runtime/slice.go

  1. type slice struct {
  2. array unsafe.Pointer // 元素指针
  3. len int // 长度
  4. cap int // 容量
  5. }

slice 的数据结构如下:
slice源码分析 - 图3

注意,底层数组是可以被多个 slice 同时指向的,因此对一个 slice 的元素进行操作是有可能影响到其他 slice 的。
【引申1】 [3]int 和 [4]int 是同一个类型吗?
不是。因为数组的长度是类型的一部分,这是与 slice 不同的一点。
【引申2】 下面的代码输出是什么?

  1. package main
  2. import "fmt"
  3. func main() {
  4. slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
  5. s1 := slice[2:5]
  6. s2 := s1[2:6:7]
  7. s2 = append(s2, 100)
  8. s2 = append(s2, 200)
  9. s1[2] = 20
  10. fmt.Println(s1)
  11. fmt.Println(s2)
  12. fmt.Println(slice)
  13. }
  14. 结果:
  15. [2 3 20]
  16. [4 5 6 7 100 200]
  17. [0 1 2 3 20 5 6 7 100 9]

说明:例子来自雨痕大佬《Go学习笔记》第四版,P43页。这里我会进行扩展,并会作图详细分析。

s1slice 索引2(闭区间)到索引5(开区间,元素真正取到索引4),长度为3,容量默认到数组结尾,为8。 s2s1 的索引2(闭区间)到索引6(开区间,元素真正取到索引5),容量到索引7(开区间,真正到索引6),为5。
slice源码分析 - 图4
接着,向 s2 尾部追加一个元素 100:
s2 = append(s2, 100)
s2 容量刚好够,直接追加。不过,这会修改原始数组对应位置的元素。这一改动,数组和 s1 都可以看得到。
slice源码分析 - 图5
再次向 s2 追加元素200:
s2 = append(s2, 100)
这时,s2 的容量不够用,该扩容了。于是,s2 另起炉灶,将原来的元素复制新的位置,扩大自己的容量。并且为了应对未来可能的 append 带来的再一次扩容,s2 会在此次扩容的时候多留一些 buffer,将新的容量将扩大为原始容量的2倍,也就是10了。
slice源码分析 - 图6
最后,修改 s1 索引为2位置的元素:
s1[2] = 20
这次只会影响原始数组相应位置的元素。它影响不到 s2 了,人家已经远走高飞了。
slice源码分析 - 图7
再提一点,打印 s1 的时候,只会打印出 s1 长度以内的元素。所以,只会打印出3个元素,虽然它的底层数组不止3个元素。

其他

  1. 汇编的生成方法

    1. go tool compile -N -S slice.go > slice.S
  2. 需要了解unsafe.Pointer 的使用

  3. slice.go 位于 runtime/slice.go
  4. 上述代码使用 go1.12.5 版本
  5. 还有一点需要提醒, type 长度为0的对象。比如说 struct{} 类型。(所以,很多使用chan struct{} 做channel 的传递,节省内存)
    1. package main
    2. import "fmt"
    3. import "unsafe"
    4. func main() {
    5. var data [100000]struct{}
    6. var data1 [100000]int
    7. // 0
    8. fmt.Println(unsafe.Sizeof(data))
    9. // 800000
    10. fmt.Println(unsafe.Sizeof(data1))
    11. }