概述
前言
熟悉 slice 的底层数据结构 - 实际存储数据的array,当前长度len与容量cap
slice的扩容机制 - 不严格来说,当长度小于1024时,cap翻倍;大于1024时,增加1/4
slice 有很多特性与 map 一致 - 记住一点,代码中操作的slice和map只是上层的,实际存储数据的是array与hmap
golang随笔之slice+append的陷阱
通过代码学习底层
package main import ( "fmt" "unsafe" ) func slice() { fmt.Println("Slice Init") var s []int // Tip: 对比一下map和slice的make函数,前者在类型后可跟0个或1个参数,而后者是1个和2个参数 s = make([]int, 2) fmt.Println(len(s), cap(s)) s = make([]int, 2, 4) fmt.Println(len(s), cap(s)) // Tip: 元素个数小于cap时,append不会改变cap,只会增加len fmt.Println("Slice Assign") s[0] = 1 s[1] = 2 s = append(s, 4) fmt.Println(len(s), cap(s)) // Tip: 元素个数超过cap时,会进行扩容 s = append(s, 8, 16) fmt.Println(len(s), cap(s)) // Tip: Slice没有显式的删除语句 fmt.Println("Slice Delete") s = append(s[0:1], s[2:]...) fmt.Println(s) fmt.Println("Slice Range") for i, v := range s { fmt.Println(i, v) fmt.Printf("%p %p\n", &i, &v) } } /* slice 的源码部分 slice基础结构slice: 包括保存数据的array、长度len与容量cap 初始化函数makeslice: math.MulUintptr:根据元素大小和容量cap,计算所需的内存空间 mallocgc: 分配内存, 32K作为一个临界值,小的分配在P的cache中,大的分配在heap堆中 扩容growslice: 当长度小于1024时,cap翻倍;大于1024时,增加1/4。 但这个并不是绝对的,会根据元素的类型尽心过一定的优化 拷贝slicecopy: 核心函数为memmove,from=>to移动size大小的数据,size为 元素大小 * from和to中长度较小的个数 拷贝slicestringcopy: 基本与上面类似,字符串的拷贝 */ func sliceAddr() { fmt.Println("Part 1") var s = make([]int, 2, 2) s[0] = 1 fmt.Println(unsafe.Pointer(&s[0])) s[1] = 2 fmt.Println(unsafe.Pointer(&s[0])) // Tip: 扩容后,slice的array的地址会重新分配 s = append(s, 3) fmt.Println(unsafe.Pointer(&s[0])) fmt.Println("Part 2") // Tip: a虽然是一个新的地址,但指向的array是和a一致的 a := s[:2] fmt.Printf("%p %p\n", &s, &a) fmt.Println(unsafe.Pointer(&a[0])) a[0] = 2 fmt.Println(a, s) // Tip: 如果要进行slice拷贝,使用copy方法 b := make([]int, 2) copy(b, s)//copy会重新分配底层array的内存 fmt.Printf("%p %p\n", &s, &b) fmt.Println(unsafe.Pointer(&b[0])) fmt.Println("Part 3") // Tip: sNil的array指向nil,而sEmpty的array指向一个内存地址 var sNil []int//底层array指向nil var sEmpty = make([]int, 0)//底层array指向一个内存地址,但是这个内存地址没有分配空间给它 fmt.Println(len(sNil), len(sEmpty), cap(sNil), cap(sEmpty)) }
详解
slice类型
在runtime下的slice.go可以看到
type slice struct { array unsafe.Pointer len int cap int }
slice基础结构包括保存数据的array、长度len与容量cap,其底层是数组array
这里的ints[3]和ints[4]不能访问,否则属于越界访问,会引发panic
扩容机制
在runtime下的slice.go内有一个扩容growslice函数
扩容机制是
- 如果期望容量大于当前容量的两倍就会使用期望容量;
- 如果当前切片的长度小于 1024 就会将容量翻倍;
- 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;
//growslice部分代码 if cap < old.cap { panic(errorString("growslice: cap out of range")) } if et.size == 0 { // append should not create a slice with nil pointer but non-zero len. // We assume that append doesn't need to preserve old.array in this case. return slice{unsafe.Pointer(&zerobase), old.len, cap} } newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { if old.cap < 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 } } } //下面还有优化
根据元素的类型做一定的优化
比如新容量是3,int类型,则它需要申请24B的内存,此时它会向语言自身的内存管理模块去申请内存,而内存管理模块会提前向操作系统申请一批内存,分为常用的规格管理起来,我们申请内存时,它会帮我们匹配到足够大,且最接近规格的内存,可能这里内存管理模块分配给你了32B的内存,所以这个时候新容量变成4个了
//runtime下sizeclasses.go文件 go采用的是基于tcmalloc进行的内存分配,也就是go语言的内存管理模块。 var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}