我正在参加「掘金·启航计划」不知道大家在刚开始学习数组和切片的时候,是否总是分不清数组和切片?我们可以类比其他语言时,总是能发现有很多相似的使用方法,比如数组都是大小不变的,切片是什么呢?在python中切片是一个范围内对应的元素。而在golang中切片是什么呢?如何扩容呢?下面我们一起来寻找这些问题的答案。
数组
在go官网中切片类型是建立在数组类型之上的一种抽象,因此在了解切片前我们一定要理解数组。
什么是数组?
相同唯一类型的元素、大小不变。
// 第一种
var arr [5]int
// 第二种
var arr = [5]int{1,3,4,2,5}
还可以通过索引来赋值
arr := [...]int{9: -1}
// 输出如下:
0 0 0 0 0 0 0 0 0 -1
上面的例子是通过元素的个数来确定数组的大小
数组的遍历
for-in
a := [5]int{2, 3, 4, 6, 7}
for i :=0; i< len(a); i++ {
fmt.Printf("%d\t", a[i])
}
for-range
a := [5]int{2, 3, 4, 6, 7}
for i,v := range a {
fmt.Printf("index:%d,value:%d\t",i,v)
}
for-range遍历中i,v分别为数组的索引和对应的值。
切片(Slice)
上面我们说到数组有着大小固定不变的特点,但是如果我们想要其中部分的元素,那么数组就不好使了,因此在go语言中切片可以灵活的改变元素个数、扩展容量的特点,切片得到广泛使用。
如何声明一个切片?
// 直接声明
var slice []int
// make 创建
slice := make([]int, 2, 5) // 2: 长度; 5:容量
// 也可以通过字面量来定义
slice := []int{1,2,3,4,5}
接下来我们还是按照老套路来分析代码在运行时是如何执行的,找到 src/runtime/slice.go的代码,下面我们将切片的数据结构,创建,扩容,append,copy的原理。
切片的数据结构
type slice struct {
array unsafe.Pointer
len int
cap int
}
slice的结构体主要包含了三个属性,数组,长度,容量。这里的数组是指向底层数组的起始位置。这里我们借用下官网的图片。
创建一个切片
s := make([]byte, 5)
其结构如下:
此时创建了一个长度为5的切片,那此时的容量是多少呢? 我们可以通过**cap()**得到对应的大小,即2。容量是从创建切片的索引开始的底层数组中元素的数量。此时切片在内存分配的大小为切片元素大小*切片的容量,然后调用mallocgc()函数进行内存分配。具体代码如下:
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)
}
零切片
如上面的方式创建切片后,此时切片的内部元素是什么?我们不妨打印出来看看
s := make([]byte, 5)
fmt.Println(s)
// 输出如下:
[0 0 0 0 0]
此时的切片称为零切片,由于其初始化后,对应元素类型的默认值赋值。当然不同类型下,默认值是不同的;int类型是0,指针类型为nil,string类型为"", bool类型为false
当我们声明切片时将切片的长度和容量设置为0,那会是怎样的情况?
空切片
当切片长度和容量均为0时,此时的切片称为空切片。数组指针指向相同一个内存地址,即unsafe.Pointer(&pointer)。
s := make([]int, 0)
切片拷贝
当声明一个切片后,现在有个需求是取其中几个元素的切片,就可以如下面的代码得到切分出对应的切片:
s := []int{1,3,5,6,7,8}
fmt.Println("before:", s)
s = s[2:4]
fmt.Println("after:", s)
//输出如下:
before: [1 3 5 6 7 8]
after: [5 6]
如上图所示,此时创建了一个指向原始数组的新切片,修改了切片的元素,并重新给原始切片赋值,改变了切片的内存地址。
当 [:]和copy()复制切片数据,看下面的代码执行结果会是怎样的呢?
s := []int{1, 3, 5, 6, 7, 8}
fmt.Printf("before:%v,add:%p\n", s, s)
s1 := s[:]
fmt.Printf("after:%v,add:%p\n", s1, s1)
s2 := make([]int, 10, 10)
copy(s2, s)
fmt.Printf("copy:%v,add:%p\n", s2, s2)
// console:
before:[1 3 5 6 7 8],add:0xc0000c8030
after:[1 3 5 6 7 8],add:0xc0000c8030
copy:[1 3 5 6 7 8 0 0 0 0],add:0xc0000ac190
从上面我们可以很容易得出以下结论:
[:] 和 赋值 = 都是拷贝数据内存地址,新的变量和旧的变量指向相同的地址,当新的变量修改切片的元素值时,旧的变量的切片的元素相应的会改变;
copy()是将数据本身拷贝一份,创建一个新的对象,拷贝的数据指向这个新对象对应的内存空间地址。新对象的变化不会影响老对象的数据。
我们将拷贝数据本身这种方式称为深拷贝;拷贝引用数据地址的方式称为浅拷贝。
append()
append 作用就是在slice里面最后一个元素后面追加一个或多个元素,然后返回最新的切片。可以类比于java中的list.add()方法。
var a = make([]string, 5, 10)
for i := 0; i < 10; i++ {
a = append(a, fmt.Sprintf("%v", i))
}
fmt.Println(a)
// console:
[ 0 1 2 3 4 5 6 7 8 9]
从上面的例子,有没有发现一个问题? 在创建切片时容量定义为10,通过循环10次在切片a上添加对应的值后,此时的切片a 已经超出最大容量,是不是应该考虑扩展其容量问题,该扩大多少呢?
还是用上面的例子来看看切片a是不是真的扩容了,我们只需要打印对应的内存地址和容量大小。代码和结果如下:
var a = make([]string, 5, 10)
for i := 0; i < len(a); i++ {
fmt.Printf("add:%p, len:%v, cap:%v\n", &a[i], len(a), cap(a))
}
for i := 0; i < 10; i++ {
a = append(a, fmt.Sprintf("%v", i))
fmt.Printf("add:%p, len:%v, cap:%v\n", a, len(a), cap(a))
}
// console:
add:0xc000000140, len:5, cap:10
add:0xc000000150, len:5, cap:10
add:0xc000000160, len:5, cap:10
add:0xc000000170, len:5, cap:10
add:0xc000000180, len:5, cap:10
add:0xc000000140, len:6, cap:10
add:0xc000000150, len:7, cap:10
add:0xc000000160, len:8, cap:10
add:0xc000000170, len:9, cap:10
add:0xc000000180, len:10, cap:10
add:0xc0000022d0, len:11, cap:20
add:0xc0000022e0, len:12, cap:20
add:0xc0000022f0, len:13, cap:20
add:0xc000002300, len:14, cap:20
add:0xc000002310, len:15, cap:20
如结果所展示,前5个是创建切片时给的默认值,从140-180生成连续的内存地址用来存放默认值;在len=6时,就是append插入的值,此时内存地址指向了140,容量依然为10,直到内存地址2d0,容量扩大了2倍,变为20。这里我们得到了两个信息:1.扩展后的切片依然指向的是原来的底层数组地址;2.扩容了2倍。到此我们通过生成汇编代码再看运行时代码是如何执行的。
CALL runtime.makeslice(SB)
...
MOVQ BX, main..autotmp_28+96(SP)
MOVQ AX, main..autotmp_29+112(SP)
LEAQ type.string(SB), AX
MOVQ main.a.ptr+104(SP), BX
MOVQ CX, DX
MOVQ SI, CX
MOVQ DX, SI
PCDATA $1, $6
CALL runtime.growslice(SB)
LEAQ 1(BX), DX
MOVQ main..autotmp_28+96(SP), BX
MOVQ main.a.len+80(SP), SI
这里我们重点放在runtime的调用函数上,找到 src/runtime/slice.go 下的 growslice 函数
// growslice 在append中处理切片的扩容
// 该函数输入切片元素的类型,旧切片和新的需要最小容量这些传入参数,
// 返回了一个新的容量和已复制旧数据的切片.
// 这个新的切片被赋值了旧切片的长度,而不是新的容量。
func growslice(et *_type, old slice, cap int) slice {
...
if cap < old.cap {
panic(errorString("growslice: cap out of range"))
}
if et.size == 0 {
// append 不应创建一个空指针长度却不为0的切片。
// 出现这种情况时,我们假定append不需要保留旧的底层数组。
return slice{unsafe.Pointer(&zerobase), old.len, cap}
}
newcap := old.cap
doublecap := newcap + newcap
// 新切片需要的最小容量大于两倍容量时,则直接为新的最小容量
if cap > doublecap {
newcap = cap
} else {
const threshold = 256
// 旧切片容量 < 256时,新切片容量扩容2倍
if old.cap < threshold {
newcap = doublecap
} else {
// >= 256,则按照扩容1.25倍
for 0 < newcap && newcap < cap {
// Transition from growing 2x for small slices
// to growing 1.25x for large slices. This formula
// gives a smooth-ish transition between the two.
newcap += (newcap + 3*threshold) / 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 goarch.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 == goarch.PtrSize:
lenmem = uintptr(old.len) * goarch.PtrSize
newlenmem = uintptr(cap) * goarch.PtrSize
capmem = roundupsize(uintptr(newcap) * goarch.PtrSize)
overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
newcap = int(capmem / goarch.PtrSize)
case isPowerOfTwo(et.size):
var shift uintptr
if goarch.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-et.size+et.ptrdata)
}
}
memmove(p, old.array, lenmem)
return slice{p, old.len, newcap}
}
扩容结论:
- 当新申请容量 > 两倍原有容量,则扩容后容量为新申请容量大小
- 当old.cap < 256, 则每次扩容为原来的 2 倍
- 当old.cap >= 256, 则每次扩容扩为原来的 1.25 倍