内容简介:切片是 Go 中的一种基本的数据结构,使用这种结构可以用来管理数据集合。切片的设计想法是由动态数组概念而来,为了开发者可以更加方便的使一个数据结构可以自动增加和减少。但是切片本身并不是动态数据或者数组指针。切片常见的操作有 reslice、append、copy。与此同时,切片还具有可索引,可迭代的优秀特性。在 Go 中,Go 数组是值类型,赋值和函数传参操作都会复制整个数组数据。打印结果:
一、概述
切片是 Go 中的一种基本的数据结构,使用这种结构可以用来管理数据集合。切片的设计想法是由动态数组概念而来,为了开发者可以更加方便的使一个数据结构可以自动增加和减少。但是切片本身并不是动态数据或者数组指针。切片常见的操作有 reslice、append、copy。与此同时,切片还具有可索引,可迭代的优秀特性。
1.切片和数组关于切片和数组怎么选择?
在 Go 中,Go 数组是值类型,赋值和函数传参操作都会复制整个数组数据。
1 func main() { 2 arrayA := [2]int{100, 200} // 定义数组并初始化内容 3 var arrayB [2]int // 定义一个数组 4 arrayB = arrayA 5 fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA) 6 fmt.Printf("arrayB : %p , %v\n", &arrayB, arrayB) 7 testArray(arrayA) 8 } 9 func testArray(x [2]int) { // 此处使用值传递 会导致地址不同 10 fmt.Printf("func Array : %p , %v\n", &x, x) 11 }
打印结果:
arrayA : 0xc4200bebf0 , [100 200] arrayB : 0xc4200bec00 , [100 200] func Array : 0xc4200bec30 , [100 200]
可以看到,三个内存地址都不同,这也就验证了 Go 中数组赋值和函数传参都是值复制的。那这会导致什么问题呢?
假想每次传参都用数组,那么每次数组都要被复制一遍。如果数组大小有 100万,在64位机器上就需要花费大约 800W bytes,即 8MB 内存。这样会消耗掉大量的内存。于是乎有人想到,函数传参用数组的指针。
1 func main() { 2 arrayA := [2]int{100, 200} 3 testArrayPoint(&arrayA) // 1.传数组指针 4 arrayB := arrayA[:] 5 testArrayPoint(&arrayB) // 2.传切片 6 fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA) 7 } 8 func testArrayPoint(x *[]int) { // 此处使用的指针,会导致函数参数和arrayB指向同一块内存 9 fmt.Printf("func Array : %p , %v\n", x, *x) 10 (*x)[1] += 100 11 }
打印结果:
func Array : 0xc4200b0140 , [100 200] func Array : 0xc4200b0180 , [100 300] arrayA : 0xc4200b0140 , [100 400]
这也就证明了数组指针确实到达了我们想要的效果。现在就算是传入10亿的数组,也只需要再栈上分配一个8个字节的内存给指针就可以了。这样更加高效的利用内存,性能也比之前的好。
不过传指针会有一个弊端,从打印结果可以看到,第一行和第三行指针地址都是同一个,万一原数组的指针指向更改了,那么函数里面的指针指向都会跟着更改。
切片的优势也就表现出来了。用切片传数组参数,既可以达到节约内存的目的,也可以达到合理处理好共享内存的问题。打印结果第二行就是切片,切片的指针和原来数组的指针是不同的。
由此我们可以得出结论:
把第一个大数组传递给函数会消耗很多内存,采用切片的方式传参可以避免上述问题。切片是引用传递,所以它们不需要使用额外的内存并且比使用数组更有效率。但是,依旧有反例。
package bench_test import "testing" func array() [1024]int { var x [1024]int for i:=0; i<len(x);i++{ x[i] = i } return x } func slice() []int{ var x = make([]int, 1024) for i:=0; i<len(x);i++{ x[i] = i } return x } func BenchmarkArray(b *testing.B){ for i := 0; i < b.N; i++{ array() } } func BenchmarkSlice(b *testing.B){ for i := 0; i < b.N; i++{ slice() } } // // 虽然相对值传递,引用传递在给函数时不需要复制整个数据;但是并不以为所有的操作都需要使用slice 上面的例子是很好的验证 // func array_param(x [81920]int) [81920]int { //var x [1024]int for i:=0; i<len(x);i++{ x[i] = i } return x } func slice_param(x []int) []int{ //var x = make([]int, 1024) for i:=0; i<len(x);i++{ x[i] = i } return x } func BenchmarkArrayParam(b *testing.B){ var x = [81920]int{} for i := 0; i < b.N; i++{ array_param(x) } } func BenchmarkSliceParam(b *testing.B){ var x = make([]int, 81920) for i := 0; i < b.N; i++{ slice_param(x) } } // // 当将数组和切片作为函数参数时 其对应的参数数据量越大 相对来说切片的的引用传递会凸显其优势 // 不过需要需要注意的slice会涉及在heap进行内存分配: // 切片底层数组可能会在堆上分配内存,这样使用数组在stack进行拷贝未必弱于make的内存分配 //
我们做一次性能测试,并且禁用内联和优化,来观察切片的堆上内存分配的情况。
go test -bench . -benchmem -gcflags -N -l
输出结果比较“令人意外”:
goos: linux goarch: amd64 pkg: gonotes/lesson-1/bench_test BenchmarkArray-8 1000000 1925 ns/op 0 B/op 0 allocs/op BenchmarkSlice-8 500000 4158 ns/op 8192 B/op 1 allocs/op BenchmarkArrayParam-8 10000 182077 ns/op 0 B/op 0 allocs/op BenchmarkSliceParam-8 10000 166897 ns/op 65 B/op 0 allocs/op PASS ok gonotes/lesson-1/bench_test 7.589s
在测试 Array 的时候,用的是8核,循环次数是1000000,平均每次执行时间是1925 ns,每次执行堆上分配内存总量是0,分配次数也是0 。
而切片的结果就“差”一点,同样也是用的是8核,循环次数是500000,平均每次执行时间是4158 ns,但是每次执行一次,堆上分配内存总量是8192,分配次数也是1 。
并非所有时候都适合用切片代替数组,因为切片底层数组可能会在堆上分配内存,而且小数组在栈上拷贝的消耗也未必比 make 消耗大。
2.切片的数据结构
切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一个只读对象,其工作机制类似数组指针的一种封装。
切片(slice)是对数组一个连续片段的引用,所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 java 中的 list 类型)。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个与指向数组的动态窗口。
给定项的切片索引可能比相关数组的相同元素的索引小。和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度:切片是一个长度可变的数组。
Slice 的数据结构定义如下:
type slice struct { array unsafe.Pointer len int cap int }
切片的结构体由3部分构成,Pointer 是指向一个数组的指针,len 代表当前切片的长度,cap 是当前切片的容量。cap 总是大于等于 len 的。
slice
内部结构
如果想从 slice 中得到一块内存地址,可以这样做:
s := make([]byte, 200) ptr := unsafe.Pointer(s[0])
如果反过来呢?从 Go 的内存地址中构造一个 slice。
var ptr unsafe.Pointer var s1 = struct { addr uintptr4 len int5 cap int6 }{ptr, length, length} s := *(*[]byte)(unsafe.Pointer(s1))
构造一个虚拟的结构体,把 slice 的数据结构拼出来。
在 Go 的反射中就存在一个与之对应的数据结构 SliceHeader,我们可以用它来构造一个 slice
var o []byte sliceHeader := (*reflect.SliceHeader)((unsafe.Pointer(o))) sliceHeader.Cap = length sliceHeader.Len = length sliceHeader.Data = uintptr(ptr)
3.创建切片
make 函数允许在运行期动态指定数组长度,绕开了数组类型必须使用编译期常量的限制。
创建切片有两种形式,make 创建切片,空切片。
3.1. make 和切片字面量
1 func makeslice(et *_type, len, cap int) slice { // 创建slice方法 2 // 根据切片的数据类型,获取切片的最大容量 3 maxElements := maxSliceCap(et.size) 4 // 比较切片的长度,长度值域应该在[0,maxElements]之间 5 if len < 0 || uintptr(len) > maxElements { 6 panic(errorString("makeslice: len out of range")) 7 } 8 // 比较切片的容量,容量值域应该在[len,maxElements]之间 9 if cap < len || uintptr(cap) > maxElements { 10 panic(errorString("makeslice: cap out of range")) 11 } 12 // 根据切片的容量申请内存 13 p := mallocgc(et.size*uintptr(cap), et, true) 14 // 返回申请好内存的切片的首地址 15 return slice{p, len, cap} 16 }
还有一个 int64 的版本:
1 func makeslice64(et *_type, len64, cap64 int64) slice { 2 len := int(len64) 3 if int64(len) != len64 { 4 panic(errorString("makeslice: len out of range")) 5 } 6 cap := int(cap64) 7 if int64(cap) != cap64 { 8 panic(errorString("makeslice: cap out of range")) 9 } 10 return makeslice(et, len, cap) 11 }
两个方法差别在于,只不过多了把 int64 转换成 int 这一步罢了。
make操作方式
上图是用 make 函数创建的一个 len = 4, cap = 6 的切片。内存空间申请了6个 int 类型的内存大小。由于 len = 4,所以后面2个暂时访问不到,但是容量还是在的。这时候数组里面每个变量都是0 。
除了 make 函数可以创建切片以外,字面量也可以创建切片。
字面量方式
这里是用字面量创建的一个 len = 6,cap = 6 的切片,这时候数组里面每个元素的值都初始化完成了。需要注意的是 [ ] 里面不要写数组的容量,因为如果写了个数以后就是数组了,而不是切片了。
图片.png
还有一种简单的字面量创建切片的方法。如上图。上图就 Slice A 创建出了一个 len = 3,cap = 3 的切片。从原数组的第二位元素(0是第一位)开始切,一直切到第四位为止(不包括第五位)。同理,Slice B 创建出了一个 len = 2,cap = 4 的切片。
3.2. nil 和空切片
nil 切片和空切片也是常用的。
空切片
nil 切片被用在很多标准库和内置函数中,描述一个不存在的切片的时候,就需要用到 nil 切片。比如函数在发生异常的时候,返回的切片就是 nil 切片。nil 切片的指针指向 nil。
空切片一般会用来表示一个空的集合。比如数据库查询,一条结果也没有查到,那么就可以返回一个空切片。
silce := make( []int , 0 ) slice := []int{ }
空切片
空切片和 nil 切片的区别在于,空切片指向的地址不是nil,指向的是一个内存地址,但是它没有分配任何内存空间,即底层元素包含0个元素。
最后需要说明的一点是。不管是使用 nil 切片还是空切片,对其调用内置函数 append,len 和 cap 的效果都是一样的。
4.切片扩容
当一个切片的容量满了,就需要扩容了。怎么扩,策略是什么?
1 func growslice(et *_type, old slice, cap int) slice { 2 if raceenabled { 3 callerpc := getcallerpc(unsafe.Pointer(&et)) 4 racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice)) 5 } 6 if msanenabled { 7 msanread(old.array, uintptr(old.len*int(et.size))) 8 } 9 if et.size == 0 { 10 // 如果新要扩容的容量比原来的容量还要小,这代表要缩容了,那么可以直接报panic了。 11 if cap < old.cap { 12 panic(errorString("growslice: cap out of range")) 13 } 14 // 如果当前切片的大小为0,还调用了扩容方法,那么就新生成一个新的容量的切片返回。 15 return slice{unsafe.Pointer(&zerobase), old.len, cap} 16 } 17 // 这里就是扩容的策略 18 newcap := old.cap 19 doublecap := newcap + newcap 20 if cap > doublecap { 21 newcap = cap 22 } else { 23 if old.len < 1024 { 24 newcap = doublecap 25 } else { 26 for newcap < cap { 27 newcap += newcap / 4 28 } 29 } 30 } 31 // 计算新的切片的容量,长度。 32 var lenmem, newlenmem, capmem uintptr 33 const ptrSize = unsafe.Sizeof((*byte)(nil)) 34 switch et.size { 35 case 1: 36 lenmem = uintptr(old.len) 37 newlenmem = uintptr(cap) 38 capmem = roundupsize(uintptr(newcap)) 39 newcap = int(capmem) 40 case ptrSize: 41 lenmem = uintptr(old.len) * ptrSize 42 newlenmem = uintptr(cap) * ptrSize 43 capmem = roundupsize(uintptr(newcap) * ptrSize) 44 newcap = int(capmem / ptrSize) 45 default: 46 lenmem = uintptr(old.len) * et.size 47 newlenmem = uintptr(cap) * et.size 48 capmem = roundupsize(uintptr(newcap) * et.size) 49 newcap = int(capmem / et.size) 50 } 51 // 判断非法的值,保证容量是在增加,并且容量不超过最大容量 52 if cap < old.cap || uintptr(newcap) > maxSliceCap(et.size) { 53 panic(errorString("growslice: cap out of range")) 54 } 55 var p unsafe.Pointer 56 if et.kind&kindNoPointers != 0 { 57 // 在老的切片后面继续扩充容量 58 p = mallocgc(capmem, nil, false) 59 // 将 lenmem 这个多个 bytes 从 old.array地址 拷贝到 p 的地址处 60 memmove(p, old.array, lenmem) 61 // 先将 P 地址加上新的容量得到新切片容量的地址,然后将新切片容量地址后面的 capmem-newlenmem 个 bytes 这块内存初始化。为之后继续 append() 操作腾出空间。 62 memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem) 63 } else { 64 // 重新申请新的数组给新切片 65 // 重新申请 capmen 这个大的内存地址,并且初始化为0值 66 p = mallocgc(capmem, et, true) 67 if !writeBarrier.enabled { 68 // 如果还不能打开写锁,那么只能把 lenmem 大小的 bytes 字节从 old.array 拷贝到 p 的地址处 69 memmove(p, old.array, lenmem) 70 } else { 71 // 循环拷贝老的切片的值 72 for i := uintptr(0); i < lenmem; i += et.size { 73 typedmemmove(et, add(p, i), add(old.array, i)) 74 } 75 } 76 } 77 // 返回最终新切片,容量更新为最新扩容之后的容量 78 return slice{p, old.len, newcap} 79 }
上述就是扩容的实现。主要需要关注的有两点,一个是扩容时候的策略,还有一个就是扩容是生成全新的内存地址还是在原来的地址后追加。
-
扩容策略
先看看扩容策略。
1 func main() { 2 slice := []int{10, 20, 30, 40} 3 newSlice := append(slice, 50) 4 fmt.Printf("Before slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice)) 5 fmt.Printf("Before newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice)) 6 newSlice[1] += 10 7 fmt.Printf("After slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice)) 8 fmt.Printf("After newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice)) 9 }
输出结果:
Before slice = [10 20 30 40], Pointer = 0xc4200b0140, len = 4, cap = 4 Before newSlice = [10 20 30 40 50], Pointer = 0xc4200b0180, len = 5, cap = 8 After slice = [10 20 30 40], Pointer = 0xc4200b0140, len = 4, cap = 4 After newSlice = [10 30 30 40 50], Pointer = 0xc4200b0180, len = 5, cap = 8
用图表示出上述过程。
执行过程
从图上我们可以很容易的看出,新的切片和之前的切片已经不同了,因为新的切片更改了一个值,并没有影响到原来的数组,新切片指向的数组是一个全新的数组。并且 cap 容量也发生了变化。这之间究竟发生了什么呢?
Go 中切片扩容的策略是这样的:
如果切片的容量小于 1024 个元素,于是扩容的时候就翻倍增加容量。上面那个例子也验证了这一情况,总容量从原来的4个翻倍到现在的8个。
一旦元素个数超过 1024 个元素,那么增长因子就变成 1.25 ,即每次增加原来容量的四分之一。
注意:扩容扩大的容量都是针对原来的容量而言的,而不是针对原来数组的长度而言的。
-
新数组 or 老数组 ?
再谈谈扩容之后的数组一定是新的么?这个不一定,分两种情况。
情况一:
1 func main() { 2 array := [4]int{10, 20, 30, 40} 3 slice := array[0:2] 4 newSlice := append(slice, 50) 5 fmt.Printf("Before slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice)) 6 fmt.Printf("Before newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice)) 7 newSlice[1] += 10 8 fmt.Printf("After slice = %v, Pointer = %p, len = %d, cap = %d\n", slice, &slice, len(slice), cap(slice)) 9 fmt.Printf("After newSlice = %v, Pointer = %p, len = %d, cap = %d\n", newSlice, &newSlice, len(newSlice), cap(newSlice)) 10 fmt.Printf("After array = %v\n", array) 11 }
打印输出:
Before slice = [10 20], Pointer = 0xc4200c0040, len = 2, cap = 4 Before newSlice = [10 20 50], Pointer = 0xc4200c0060, len = 3, cap = 4 After slice = [10 30], Pointer = 0xc4200c0040, len = 2, cap = 4 After newSlice = [10 30 50], Pointer = 0xc4200c0060, len = 3, cap = 4 After array = [10 30 50 40]
把上述过程用图表示出来,如下图。
实例执行过程
通过打印的结果,我们可以看到,在这种情况下,扩容以后并没有新建一个新的数组,扩容前后的数组都是同一个,这也就导致了新的切片修改了一个值,也影响到了老的切片了。并且 append() 操作也改变了原来数组里面的值。一个 append() 操作影响了这么多地方,如果原数组上有多个切片,那么这些切片都会被影响!无意间就产生了莫名的 bug!
这种情况,由于原数组还有容量可以扩容,所以执行 append() 操作以后,会在原数组上直接操作,所以这种情况下,扩容以后的数组还是指向原来的数组。
这种情况也极容易出现在字面量创建切片时候,第三个参数 cap 传值的时候,如果用字面量创建切片,cap 并不等于指向数组的总容量,那么这种情况就会发生。
slice := array[1:2:3]
上面这种情况非常危险,极度容易产生 bug 。
建议用字面量创建切片的时候,cap 的值一定要保持清醒,避免共享原数组导致的 bug。
情况二:
情况二其实就是在扩容策略里面举的例子,在那个例子中之所以生成了新的切片,是因为原来数组的容量已经达到了最大值,再想扩容, Go 默认会先开一片内存区域,把原来的值拷贝过来,然后再执行 append() 操作。这种情况丝毫不影响原数组。
所以建议尽量避免情况一,尽量使用情况二,避免 bug 产生。
五. 切片拷贝
Slice 中拷贝方法有2个。
1 func slicecopy(to, fm slice, width uintptr) int { 2 // 如果源切片或者目标切片有一个长度为0,那么就不需要拷贝,直接 return 3 if fm.len == 0 || to.len == 0 { 4 return 0 5 } 6 // n 记录下源切片或者目标切片较短的那一个的长度 7 n := fm.len 8 if to.len < n { 9 n = to.len 10 } 11 // 如果入参 width = 0,也不需要拷贝了,返回较短的切片的长度 12 if width == 0 { 13 return n 14 } 15 // 如果开启了竞争检测 16 if raceenabled { 17 callerpc := getcallerpc(unsafe.Pointer(&to)) 18 pc := funcPC(slicecopy) 19 racewriterangepc(to.array, uintptr(n*int(width)), callerpc, pc) 20 racereadrangepc(fm.array, uintptr(n*int(width)), callerpc, pc) 21 } 22 // 如果开启了 The memory sanitizer (msan) 23 if msanenabled { 24 msanwrite(to.array, uintptr(n*int(width))) 25 msanread(fm.array, uintptr(n*int(width))) 26 } 27 size := uintptr(n) * width 28 if size == 1 { 29 // TODO: is this still worth it with new memmove impl? 30 // 如果只有一个元素,那么指针直接转换即可 31 *(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer 32 } else { 33 // 如果不止一个元素,那么就把 size 个 bytes 从 fm.array 地址开始,拷贝到 to.array 地址之后 34 memmove(to.array, fm.array, size) 35 } 36 return n 37 }
在这个方法中,slicecopy 方法会把源切片值(即 fm Slice )中的元素复制到目标切片(即 to Slice )中,并返回被复制的元素个数,copy 的两个类型必须一致。slicecopy 方法最终的复制结果取决于较短的那个切片,当较短的切片复制完成,整个复制过程就全部完成了。
copy过程
举个例子,比如:
1 func main() { 2 array := []int{10, 20, 30, 40} 3 slice := make([]int, 6) 4 n := copy(slice, array) 5 fmt.Println(n,slice) 6 }
还有一个拷贝的方法,这个方法原理和 slicecopy 方法类似,不在赘述了,注释写在代码里面了。
1 func slicestringcopy(to []byte, fm string) int { 2 // 如果源切片或者目标切片有一个长度为0,那么就不需要拷贝,直接 return 3 if len(fm) == 0 || len(to) == 0 { 4 return 0 5 } 6 // n 记录下源切片或者目标切片较短的那一个的长度 7 n := len(fm) 8 if len(to) < n { 9 n = len(to) 10 } 11 // 如果开启了竞争检测 12 if raceenabled { 13 callerpc := getcallerpc(unsafe.Pointer(&to)) 14 pc := funcPC(slicestringcopy) 15 racewriterangepc(unsafe.Pointer(&to[0]), uintptr(n), callerpc, pc) 16 } 17 // 如果开启了 The memory sanitizer (msan) 18 if msanenabled { 19 msanwrite(unsafe.Pointer(&to[0]), uintptr(n)) 20 } 21 // 拷贝字符串至字节数组 22 memmove(unsafe.Pointer(&to[0]), stringStructOf(&fm).str, uintptr(n)) 23 return n 24 }
再举个例子,比如:
1 func main() { 2 slice := make([]byte, 3) 3 n := copy(slice, "abcdef") 4 fmt.Println(n,slice) 5 }
输出:
13 [97,98,99]
说到拷贝,切片中有一个需要注意的问题。
1 func main() { 2 slice := []int{10, 20, 30, 40} 3 for index, value := range slice { 4 fmt.Printf("value = %d , value-addr = %x , slice-addr = %x\n", value, &value, &slice[index]) 5 } 6 }
输出:
value = 10 , value-addr = c4200aedf8 , slice-addr = c4200b0320 value = 20 , value-addr = c4200aedf8 , slice-addr = c4200b0328 value = 30 , value-addr = c4200aedf8 , slice-addr = c4200b0330 value = 40 , value-addr = c4200aedf8 , slice-addr = c4200b0338
从上面结果我们可以看到,如果用 range 的方式去遍历一个切片,拿到的 Value 其实是切片里面的值拷贝。所以每次打印 Value 的地址都不变。
vale的值拷贝
由于 Value 是值拷贝的,并非引用传递,所以直接改 Value 是达不到更改原切片值的目的的,需要通过slice[index] 获取真实的地址。
六、简单demo
package main import ( "fmt" "unsafe" ) func main() { xx := []int{100, 200, 300, 400, 500, 600, 700, 800} xxx := xx[2:6] fmt.Printf("xx's address is %p, %v\n", &xx, xx) fmt.Printf("xxx's address is %p, %v\n", &xxx, xxx) xxx[2] += 1000 // 由于指向同一块内存会影响xx原有的内容 fmt.Printf("after updated, xxx's address %p, %v\n", &xxx, xxx) fmt.Printf("after updated, xx's address %p, %v\n", &xx, xx) fmt.Println("======================================") x := make([]int,0 ,5) fmt.Println(unsafe.Pointer(&x)) fmt.Printf("before append x's address =%p\n", &x) for i := 0; i<8; i++{ x = append(x,i) } fmt.Printf("after append x's address =%p\n", &x) fmt.Println(unsafe.Pointer(&x)) fmt.Println("======================================") // 扩容都是在原有的地址上进行追加 也就会导致扩容前后内存地址是不变的 slice1 := make([]int, 0) fmt.Printf("slice1's address is %p \n", &slice1) for i := 0; i<1024; i++{ slice1 = append(slice1,i) } fmt.Printf("after append slice1's address is %p\n", &slice1) fmt.Println("======================================") // 扩容都是在原有的地址上进行追加 也就会导致扩容前后内存地址是不变的 slice2 := []int{10,20,30,40} newslice := append(slice2, 50) fmt.Printf("slice2 address=%p, %v\n", &slice2, slice2) fmt.Printf("newslice address=%p, %v\n", &newslice, newslice) newslice[1] += 100 fmt.Printf("After update,slice2 address=%p, %v\n", &slice2, slice2) fmt.Printf("After update,newslice address=%p, %v\n", &newslice, newslice) } // // 空切片: make([]int, 0) / []int{} 代表创建容量为0的slice,故而其会指向一块内存地址,不过该内存地址没有分配任何空间的 // nil切片:地址为nil; // 空切片和nil切片是不相同的;不过两者对调用内置函数 append,len 和 cap 的效果都是一样的。 // // 当slice本身的容量已满的情况下 涉及到了扩容,只要会涉及如下内容 // 1、扩容策略 // 需要扩容的大小超过了原有大小的2*old_slice,则直接使用申请的大小 // 若是申请大小<=2*old_slice: // 当old_slice < 1024 按照2*old_slice进行扩容 // 当old_slice >= 1024 按照1.25 * old_slice进行扩容 // 根据提供的扩容策略来计算新切片的容量和大小: // 首先使用mallocgc在old后面进行扩充容量 // 其次见old内容copy到p地址处 // 最后得到新的切片容量地址 = P地址 + 新增扩容大小;初始化(capmem-newlenmen)间的内存地址初始化 // 2、在原有地址上进行追加(扩容的地址和原有的地址保持不变) // 当申请的slice类型与old相同并且kind属于非pointer 则在old基础上进行追加mallogc // 反之则重新申请新的底层数组给新的切片 // 首先重新申请新的数组给到新的切片:重新申请capmem的内存地址,并进行初始化 // 其次进行写锁检查:当写锁开启则通过循环copy老切片的内容;若是为开启写锁则只能将lenmen大小的字节从old数组copy到p的地址处 // 3、返回申请的slice大小 //
以上所述就是小编给大家介绍的《golang之slice剖析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 【Java集合源码剖析】ArrayList源码剖析
- Java集合源码剖析:TreeMap源码剖析
- 【剖析 | SOFARPC 框架】系列之 SOFARPC 优雅关闭剖析
- 【剖析 | SOFARPC 框架】系列之 SOFARPC 注解支持剖析
- 【剖析 | SOFARPC 框架】系列之 SOFARPC 泛化调用实现剖析
- 剖析 SOFARPC 框架系列之 SOFARPC 泛化调用实现剖析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。