type slice struct {
array unsafe.Pointer // 指向数组的指针
len int // 切片长度
cap int // 切片容量
}
切片我的理解是,他是数组的一个片段,就是说他是数组上截取的一部分,它可以通过下标访问到具体的元素,也能在原数组中自由活动
,当你往里面append
新元素的时候,如果当前数组容量足够,还会修改原数组的值;如果容量不够了,它就会把原数组中的值拷贝到一个新的数组中(新数组的容量按扩容规则增加),然后新加的元素加入到新的数组中,此时切片的底层数组就不再是原来的数组了,这个时候,你修改切片中的值,原数组就不会发生变化,因为他们不再有关联了,只是对于使用者来说,我们察觉不到这个过程.
下面我们用几个普通的例子来试试创建slice
func createSlice() {
s := make([]int, 0)
s = append(s, 1)
fmt.Println(s)
var s2 []int
s2 = append(s2, 1)
fmt.Println(s2)
s3 := []int{}
s3 = append(s3, 1)
fmt.Println(s3)
}
/**
输出结果为:
[1]
[1]
[1]
*/
上面的例子还是很好理解的,那我们接下来看看这几种创建模式的差异:
go tool compile -S main.go
使用上述命令,查看上面的代码在底层都做了些什么?
完整的返回我这边就不展示了,就展示关键的几行代码:
0x0021 00033 (main.go:6) LEAQ type.int(SB), AX
0x0028 00040 (main.go:6) PCDATA $0, $0
0x0028 00040 (main.go:6) MOVQ AX, (SP)
0x002c 00044 (main.go:6) XORPS X0, X0
0x002f 00047 (main.go:6) MOVUPS X0, 8(SP)
s := make([]int, 0)
0x0034 00052 (main.go:6) CALL runtime.makeslice(SB)
......
0x003e 00062 (main.go:7) LEAQ type.int(SB), CX
......
s = append(s, 1)
0x005f 00095 (main.go:7) CALL runtime.growslice(SB)
......
0x008c 00140 (main.go:8) CALL runtime.convTslice(SB) 这个为fmt.Println的具体实现,这里就不多说了
============================================================
0x00e7 00231 (main.go:11) LEAQ type.int(SB), AX
......
var s2 []int
0x019f 00415 (main.go:15) LEAQ runtime.zerobase(SB), AX
......
s2 = append(s2, 1)
0x010c 00268 (main.go:11) CALL runtime.growslice(SB)
......
0x0139 00313 (main.go:12) CALL runtime.convTslice(SB)
从上面我们不难看出,不同的切片创建方法,底层做了不同的操作第一个是runtime.makeslice
,第二个没有对应的实现,第三个是runtime.zerobase
,这三种方式区别还是有的,但是最终都可以实现我们的目标,因为最终都是调用了runtime.mallocgc
函数,也就是分配内存,看到这有同学就会问了,第二种方式我们并没有对应的实现啊,也就是说并没有分配内存啊。是的,第二种方式我们确实没有直接分配内存,并且我们直接append
了,其实是因为append
操作会对没有分配内存的切片再分配一下内存(这个我们下面会说到)。所以我们在写代码时,使用 var
关键字声明切片是完全可以的,并且对于长度为0的切片我们非常建议这样声明。
现在我们先看下runtime.makeslice
:
// et为元素类型
func makeslice(et *_type, len, cap int) unsafe.Pointer {
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
// NOTE: Produce a 'len out of range' error instead of a
// 'cap out of range' error when someone does make([]T, bignumber).
// 'cap out of range' is true too, but since the cap is only being
// supplied implicitly, saying len is clearer.
// See golang.org/issue/4085.
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
return mallocgc(mem, et, true)
}
// 首先程序会根据你元素的大小以及容量做个计算(mem=size*cap)以及判断是否溢出,如果溢出了,或者大于了最大可分配的内存大小(与操作系统位数有关),以及长度小于0或者长度小于容量,就会报错.然后下面会再去判断具体是长度错误还是容量错误
// return mallocgc(mem, et, true) 才是真正去分配内存
网上关于切片的扩容都是什么切片容量小于1024的时候,新切片的容量翻倍,大于1024之后,新切片的容量为旧容量的1.25倍,但是实际上真的是这样的吗?我们先看一段代码:
slice := make([]int32,3,3)
fmt.Println("len:", len(slice), "cap:", cap(slice))
slice = append(slice, 1)
fmt.Println("len:", len(slice), "cap:", cap(slice))
fmt.Println(slice)
/**
输出结果如下:
len: 3 cap: 3
len: 4 cap: 8
[0 0 0 1]
/
从上面的例子看好像并不是翻倍耶(我的同事说因为3+1=4 然后翻倍就变成了8,哈哈哈开个玩笑哈),那么这是怎么一回事呢?我们看看源码:
func growslice(et *_type, old slice, cap int) slice {
if raceenabled {
callerpc := getcallerpc()
racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice))
}
if msanenabled {
msanread(old.array, uintptr(old.len*int(et.size)))
}
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.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
}
}
}
var overflow bool
var lenmem, newlenmem, capmem uintptr
// Specialize for common values of et.size.
// For 1 we don't need any division/multiplication.
// For sys.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
// For powers of 2, use a variable shift.
switch {
case et.size == 1:
lenmem = uintptr(old.len)
newlenmem = uintptr(cap)
capmem = roundupsize(uintptr(newcap))
overflow = uintptr(newcap) > maxAlloc
newcap = int(capmem)
case et.size == sys.PtrSize:
lenmem = uintptr(old.len) * sys.PtrSize
newlenmem = uintptr(cap) * sys.PtrSize
capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
newcap = int(capmem / sys.PtrSize)
case isPowerOfTwo(et.size):
var shift uintptr
if sys.PtrSize == 8 {
// Mask shift for better code generation.
shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
} else {
shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
}
lenmem = uintptr(old.len) << shift
newlenmem = uintptr(cap) << shift
capmem = roundupsize(uintptr(newcap) << shift)
overflow = uintptr(newcap) > (maxAlloc >> shift)
newcap = int(capmem >> shift)
default:
lenmem = uintptr(old.len) * et.size
newlenmem = uintptr(cap) * et.size
capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
capmem = roundupsize(capmem)
newcap = int(capmem / et.size)
}
// The check of overflow in addition to capmem > maxAlloc is needed
// to prevent an overflow which can be used to trigger a segfault
// on 32bit architectures with this example program:
//
// type T [1<<27 + 1]int64
//
// var d T
// var s []T
//
// func main() {
// s = append(s, d, d, d, d)
// print(len(s), "\n")
// }
if overflow || capmem > maxAlloc {
panic(errorString("growslice: cap out of range"))
}
var p unsafe.Pointer
if et.ptrdata == 0 {
p = mallocgc(capmem, nil, false)
// The append() that calls growslice is going to overwrite from old.len to cap (which will be the new length).
// Only clear the part that will not be overwritten.
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
// Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
p = mallocgc(capmem, et, true)
if lenmem > 0 && writeBarrier.enabled {
// Only shade the pointers in old.array since we know the destination slice p
// only contains nil pointers because it has been cleared during alloc.
bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem)
}
}
memmove(p, old.array, lenmem)
return slice{p, old.len, newcap}
}
从这里我们可以看出好像的确是按网上说的规则进行的扩容:
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = 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
}
}
}
在这里我们拿到了新的cap应该是6才对是吧,但是为什么就变成了8呢?我们继续往下看:
switch {
case et.size == 1:
lenmem = uintptr(old.len)
newlenmem = uintptr(cap)
capmem = roundupsize(uintptr(newcap))
overflow = uintptr(newcap) > maxAlloc
newcap = int(capmem)
case et.size == sys.PtrSize:
lenmem = uintptr(old.len) * sys.PtrSize
newlenmem = uintptr(cap) * sys.PtrSize
capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
newcap = int(capmem / sys.PtrSize)
case isPowerOfTwo(et.size):
var shift uintptr
if sys.PtrSize == 8 {
// Mask shift for better code generation.
shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
} else {
shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
}
lenmem = uintptr(old.len) << shift
newlenmem = uintptr(cap) << shift
capmem = roundupsize(uintptr(newcap) << shift)
overflow = uintptr(newcap) > (maxAlloc >> shift)
newcap = int(capmem >> shift)
default:
lenmem = uintptr(old.len) * et.size
newlenmem = uintptr(cap) * et.size
capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
capmem = roundupsize(capmem)
newcap = int(capmem / et.size)
}
它会根据你元素的类型从新给你计算一下你的新容量,这个时候得到的新容量才是你切片的真正容量,所以即使你没超过1024,也不一定就是翻倍了,而是根据你的元素类型的计算的.(int32
是4个字节的元素,在这里4个字节的元素计算起来较为麻烦,这里就不多叙述了,下面我用一个int类型的元素详细说说)
我们接下来再看个例子:(我的电脑是64位)
slice := make([]int64, 3,3)
fmt.Println("len:", len(slice), "cap:", cap(slice)) // 3 3
slice = append(slice, 1)
fmt.Println("len:", len(slice), "cap:", cap(slice))// 4 6
var a int64
fmt.Println(unsafe.Sizeof(a)) // 8
这个时候就是翻倍了,我们按上面的说法计算一下试试:
newcap := old.cap
=>newcap = 3
doublecap := newcap + newcap
=> doublecap = 6
cap
为新切片的容量即:旧切片容量3 + 新append
的1个元素=4
cap < doublecap && old.len < 1024
所以newcap = doublecap
=> newcap = 6
由上我们可知int64
为8位,我们现在再来这个switch
语句中看看
此时我们进入的是case et.size == sys.PtrSize
这个语句里(这个sys.PtrSize
与你的系统位数有关,我这边得到的值是8)
lenmem
与newlenmem
我们可以先不关心,这个是用于把原切片中的元素拷贝到新的切片中去而使用的
capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
,这个roundupsize
其实是一个内存对齐的过程
go中分配内存的规则如下(由源码我们可知roundupsize
的传参是6*8=48,刚好有对应的48与之对应):
// class bytes/obj bytes/span objects tail waste max waste
// 1 8 8192 1024 0 87.50%
// 2 16 8192 512 0 43.75%
// 3 32 8192 256 0 46.88%
// 4 48 8192 170 32 31.52%
// 5 64 8192 128 0 23.44%
// 6 80 8192 102 32 19.07%
// 7 96 8192 85 32 15.95%
// 8 112 8192 73 16 13.56%
// 9 128 8192 64 0 11.72%
// 10 144 8192 56 128 11.82%
// 11 160 8192 51 32 9.73%
// 12 176 8192 46 96 9.59%
// 13 192 8192 42 128 9.25%
// 14 208 8192 39 80 8.12%
// 15 224 8192 36 128 8.15%
// 16 240 8192 34 32 6.62%
// 17 256 8192 32 0 5.86%
// 18 288 8192 28 128 12.16%
// 19 320 8192 25 192 11.80%
// 20 352 8192 23 96 9.88%
// 21 384 8192 21 128 9.51%
// 22 416 8192 19 288 10.71%
// 23 448 8192 18 128 8.37%
// 24 480 8192 17 32 6.82%
// 25 512 8192 16 0 6.05%
// 26 576 8192 14 128 12.33%
// 27 640 8192 12 512 15.48%
// 28 704 8192 11 448 13.93%
// 29 768 8192 10 512 13.94%
// 30 896 8192 9 128 15.52%
// 31 1024 8192 8 0 12.40%
// 32 1152 8192 7 128 12.41%
// 33 1280 8192 6 512 15.55%
// 34 1408 16384 11 896 14.00%
// 35 1536 8192 5 512 14.00%
// 36 1792 16384 9 256 15.57%
// 37 2048 8192 4 0 12.45%
// 38 2304 16384 7 256 12.46%
// 39 2688 8192 3 128 15.59%
// 40 3072 24576 8 0 12.47%
// 41 3200 16384 5 384 6.22%
// 42 3456 24576 7 384 8.83%
// 43 4096 8192 2 0 15.60%
// 44 4864 24576 5 256 16.65%
// 45 5376 16384 3 256 10.92%
// 46 6144 24576 4 0 12.48%
// 47 6528 32768 5 128 6.23%
// 48 6784 40960 6 256 4.36%
// 49 6912 49152 7 768 3.37%
// 50 8192 8192 1 0 15.61%
// 51 9472 57344 6 512 14.28%
// 52 9728 49152 5 512 3.64%
// 53 10240 40960 4 0 4.99%
// 54 10880 32768 3 128 6.24%
// 55 12288 24576 2 0 11.45%
// 56 13568 40960 3 256 9.99%
// 57 14336 57344 4 0 5.35%
// 58 16384 16384 1 0 12.49%
// 59 18432 73728 4 0 11.11%
// 60 19072 57344 3 128 3.57%
// 61 20480 40960 2 0 6.87%
// 62 21760 65536 3 256 6.25%
// 63 24576 24576 1 0 11.45%
// 64 27264 81920 3 128 10.00%
// 65 28672 57344 2 0 4.91%
// 66 32768 32768 1 0 12.50%
capmem
最后返回的值是48,因此newcap = 48 / 8= 6
有的盆友可能就会说了,你这个用例没有说服力啊,他还是翻倍了呀!,好,我给你举一个不是翻倍的例子:
slice := []int64{1,2}
fmt.Println("len:", len(slice), "cap:", cap(slice)) // 2 2
slice = append(slice, 3)
fmt.Println("len:", len(slice), "cap:", cap(slice)) // 3 4
slice = append(slice, 4)
fmt.Println("len:", len(slice), "cap:", cap(slice)) // 4 4
slice = append(slice, 5)
fmt.Println("len:", len(slice), "cap:", cap(slice)) // 5 8
var a int64
fmt.Println(unsafe.Sizeof(a)) // 8
这个咋一看没问题,都翻倍了是吧,我们接着往下看:
slice := []int64{1,2}
fmt.Println("len:", len(slice), "cap:", cap(slice)) // 2 2
slice = append(slice, 3,4,5)
fmt.Println("len:", len(slice), "cap:", cap(slice)) // 5 6
这又是什么鬼???
我们接着往下看源码:
newcap := old.cap // 2
doublecap := newcap + newcap // 4
// cap = 2+3 = 5
if cap > doublecap {
newcap = cap
}
此时newcap = 5
然后计算capmem:
capmem = roundupsize(uintptr(newcap) * sys.PtrSize) // uintptr(newcap) * sys.PtrSize = 5 * 8 =40
根据上面内存分配的规则,可知32 < 40 < 48
此时向上取整,实际分配内存为48
所以newcap = int(capmem / sys.PtrSize) = 48/8 = 6
当然go的内存分配没这么简单,这里只讲述与slice
相关的部分,至此你应该知道slice
的扩容的基本原理了吧
我们还要注意的是,扩容时如果容量大于原有数据的长度,我们重新分配内存,其操作不会影响原有的数据。但是没有分配新的内存,也就是说还是原来数组的基础上添加元素,那么新的切片操作就会影响原有的数组。这部分依然不再赘述,看一下下面代码,大家都明白了:
s := []int{1,2,3,4,5,6}
fmt.Println(len(s), cap(s)) // 6 6
s2 := s[1:3]
fmt.Println(len(s2), cap(s2))// 2 5
这个时候s2的底层数组是截取的s的一部分,所以虽然他的长度是2,但是容量是5(从第二个元素开始)
s3 := s2[1:]
fmt.Println(len(s3), cap(s3)) // 1 4
这个时候s3又是截取的s2的一部分,但是他们底层数组是同一个,所以只是在s2的基础上后移了一个元素
s3[0] = 9
fmt.Println(s) // [1 2 9 4 5 6] 这个时候,s3的第一个元素是s的第三个元素,所以s数组改变了
s3 = append(s3, 7)
fmt.Println(s) // [1 2 9 7 5 6] s3的容量足够,不扩容,因此s3的后一位append了7,所以s的第4位变成了7
fmt.Println(s3) // [9 7]
s3 = append(s3,8,7,10,11) 此时,s3的容量不足,发生了扩容,底层数组发生了改变
fmt.Println(s) // [1 2 9 7 5 6] 底层数组不再改动
fmt.Println(s3) // [9 7 8 7 10 11] s3正常append了新元素
切片的拷贝分为2种,一种是浅拷贝,一种是深拷贝;浅拷贝这边就不多说了,感兴趣的可以自行学习;我们重点说一下切片的深拷贝,这里我们需要使用切片内置的一个方法copy
s := []int{1, 2, 3, 4}
var s1 []int
copy(s1, s)
fmt.Println(s1) // []
fmt.Println(s) // [1 2 3 4]
s1 = make([]int, 2)
count := copy(s1, s)
fmt.Println(s1) // [1 2]
fmt.Println(s) // [1 2 3 4]
fmt.Println(count) // 2
s1[0] = 5
fmt.Println(s1) // [5 2]
fmt.Println(s) // [1 2 3 4]
从上面可知,copy
只会拷贝目标切片长度个元素,并且 copy 后两个切片是没有影响的。