内部数据结构
@(src/runtime/slice.go)
slice 仅有三个字段,其中array 是保存数据的部分,len 字段为长度,cap 为容量。
type slice struct {
array unsafe.Pointer // 数据部分
len int // 长度
cap int // 容量
}
创建
创建一个slice,其实就是分配内存。cap, len 的设置在汇编中完成。
下面的代码主要是做了容量大小的判断,以及内存的分配。
@(src/runtime/slice.go)
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 {
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}
// 分配内存
// 小对象从当前P 的cache中空闲数据中分配
// 大的对象 (size > 32KB) 直接从heap中分配
// runtime/malloc.go
return mallocgc(mem, et, true)
}
append
对于不需要内存扩容的slice,直接数据拷贝即可。
注意:append是在现有的长度上追加
func main() {
a := make([]int,10,100)
a = append(a,1)
fmt.Println(a) // [0 0 0 0 0 0 0 0 0 0 1]
}
上面的DX 存放的就是array 指针,AX 是数据的偏移. 将 123 存入数组。
而对于容量不够的情况,就需要对slice 进行扩容。这也是slice 比较关心的地方。 (因为对于大slice,grow slice会影响到内存的分配和执行的效率)
@(src/runtime/slice.go)
func growslice(et *_type, old slice, cap int) slice {
// 静态分析, 内存扫描
// ...
if cap < old.cap {
panic(errorString("growslice: cap out of range"))
}
// 如果存储的类型空间为0, 比如说 []struct{}, 数据为空,长度不为空
if et.size == 0 {
return slice{unsafe.Pointer(&zerobase), old.len, cap}
}
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
// 如果新容量大于原有容量的两倍,则直接按照新增容量大小申请
newcap = cap
} else {
if old.len < 1024 {
// 如果原有长度小于1024,那新容量是老容量的2倍
newcap = doublecap
} else {
// 按照原有容量的1/4 增加,直到满足新容量的需要
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// 通过校验newcap 大于0检查容量是否溢出。
if newcap <= 0 {
newcap = cap
}
}
}
var overflow bool
var lenmem, newlenmem, capmem uintptr
// 为了加速计算(少用除法,乘法)
// 对于不同的slice元素大小,选择不同的计算方法
// 获取需要申请的内存大小。
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)
}
// 判断是否会溢出
if overflow || capmem > maxAlloc {
panic(errorString("growslice: cap out of range"))
}
// 内存分配
var p unsafe.Pointer
if et.kind&kindNoPointers != 0 {
p = mallocgc(capmem, nil, false)
// 清空不需要数据拷贝的部分内存
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 writeBarrier.enabled { // gc 相关
// 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}
}
切片拷贝 (copy)
切片的浅拷贝
直接切就是浅拷贝
func main() {
a := make([]int,10,100)
a1 := a[9:12:12]
a1[0] = 100
fmt.Println(a) // [0 0 0 0 0 0 0 0 0 100]
}
切片的深拷贝
深拷贝也比较简单,只是做了一次内存的深拷贝。
对于使用者角度,用copy即可
func slicecopy(to, fm slice, width uintptr) int {
if fm.len == 0 || to.len == 0 {
return 0
}
n := fm.len
if to.len < n {
n = to.len
}
// 元素大小为0,则直接返回
if width == 0 {
return n
}
// 竟态分析和内存扫描
// ...
size := uintptr(n) * width
// 直接内存拷贝
if size == 1 { // common case worth about 2x to do here
*(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer
} else {
memmove(to.array, fm.array, size)
}
return n
}
// 字符串slice的拷贝
func slicestringcopy(to []byte, fm string) int {
if len(fm) == 0 || len(to) == 0 {
return 0
}
n := len(fm)
if len(to) < n {
n = len(to)
}
// 竟态分析和内存扫描
// ...
memmove(unsafe.Pointer(&to[0]), stringStructOf(&fm).str, uintptr(n))
return n
}
引用类型作为参数时传递的是地址还是值?
在 Golang 中函数之间传递变量时总是以值的方式传递的,无论是 int,string,bool,array 这样的内置类型(或者说原始的类型),还是 slice,channel,map 这样的引用类型,在函数间传递变量时,都是以值的方式传递,也就是说传递的都是值的副本
感觉值传递示例:
func main() {
slice := []int{1, 2, 3, 4, 5}
fmt.Printf("切片:%v\n", slice)
setSliceNil(slice)
fmt.Printf("切片:%v\n", slice)
}
func setSliceNil(slice []int) {
slice = nil
fmt.Printf("切片:%v\n", slice)
}
切片:[1 2 3 4 5]
切片:[]
切片:[1 2 3 4 5]
经过函数中对slice赋值nil并没有改变main中的slice变量的值,说明go中引用类型的传递也是值传递
感觉指针传递示例
func main() {
slice := []int{1, 2, 3, 4, 5}
fmt.Printf("切片:%v\n", slice)
changeSlice(slice)
fmt.Printf("切片:%v\n", slice)
}
func changeSlice(slice []int) {
slice[2] = 10
fmt.Printf("切片:%v\n", slice)
}
切片:[1 2 3 4 5]
切片:[1 2 10 4 5]
切片:[1 2 10 4 5]
这里的结果会让人产生困惑,既然是值传递为什么main函数中的切片被修改了呢
golang所有参数传递都是值传递
为什么都是值传递,会有指针传递的效果,这个得从源码的角度来说
以切片为例:定义一个切片返回的是一个指针
type slice struct {
array unsafe.Pointer // 数据部分
len int // 长度
cap int // 容量
}
func makeslice(et *_type, len, cap int) unsafe.Pointer {...}
注意:1.10版本之前makeslice返回的是一个slice
func makeslice(et *_type, len, cap int) slice {...}
这个问题,让我纠结了很久,这里的makeslice实际上创建的是底层数组(也就是slice.array),
注意:在reflect包也解释了,SliceHeader才是slice运行的结构
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
我以为,map与slice传递的都是指针类型,实际上不是这样的,例如下面
type a struct {
a []int64
}
type b struct {
b []int64
}
func main() {
a := a{make([]int64, 10, 100)}
a.a[0] = 1
fmt.Printf("%p\n", a.a)
b := b{a.a}
fmt.Printf("%p\n", b.b)
b.b = append(b.b, 1, 2, 3)
b.b[0] = 0
fmt.Printf("%p\n", b.b)
fmt.Println(b.b)
fmt.Println(a.a)
}
0xc000070000
0xc000070000
0xc000070000
[0 0 0 0 0 0 0 0 0 0 1 2 3]
[0 0 0 0 0 0 0 0 0 0]
可以看到地址没变,b.b的长度增长没影响到a.a 但a.a[0] 却受到了 b.b[0]=0的影响
或下面
func main() {
slice := make([]int, 10, 20)
fmt.Printf("%p\n", slice) // 0xc00008e000
bbb(slice)
fmt.Println(len(slice)) // 10
}
func bbb(slice []int) {
fmt.Printf("%p\n", slice) // 0xc00008e000
for v := 0; v < 10; v++ {
slice = append(slice, v)
}
// 虽然这里的地址没变,但不会影响到main中的slice
// 为什么append后地址没变,是因为main中的slice初始化时,容量给的较大
// 这里我们在循环中,v改大一点,是会导致slice底层的数组重新分配一次内存
fmt.Printf("%p\n", slice) // 0xc00008e000
fmt.Println(len(slice)) //20
}
问题来了,为什么地址是一样的,但是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
type slice struct {
array unsafe.Pointer // 元素指针
len int // 长度
cap int // 容量
}
注意,底层数组是可以被多个 slice 同时指向的,因此对一个 slice 的元素进行操作是有可能影响到其他 slice 的。
【引申1】 [3]int 和 [4]int 是同一个类型吗?
不是。因为数组的长度是类型的一部分,这是与 slice 不同的一点。
【引申2】 下面的代码输出是什么?
package main
import "fmt"
func main() {
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := slice[2:5]
s2 := s1[2:6:7]
s2 = append(s2, 100)
s2 = append(s2, 200)
s1[2] = 20
fmt.Println(s1)
fmt.Println(s2)
fmt.Println(slice)
}
结果:
[2 3 20]
[4 5 6 7 100 200]
[0 1 2 3 20 5 6 7 100 9]
说明:例子来自雨痕大佬《Go学习笔记》第四版,P43页。这里我会进行扩展,并会作图详细分析。
s1
从 slice
索引2(闭区间)到索引5(开区间,元素真正取到索引4),长度为3,容量默认到数组结尾,为8。 s2
从 s1
的索引2(闭区间)到索引6(开区间,元素真正取到索引5),容量到索引7(开区间,真正到索引6),为5。
接着,向 s2
尾部追加一个元素 100:
s2 = append(s2, 100)s2
容量刚好够,直接追加。不过,这会修改原始数组对应位置的元素。这一改动,数组和 s1
都可以看得到。
再次向 s2
追加元素200:
s2 = append(s2, 100)
这时,s2
的容量不够用,该扩容了。于是,s2
另起炉灶,将原来的元素复制新的位置,扩大自己的容量。并且为了应对未来可能的 append
带来的再一次扩容,s2
会在此次扩容的时候多留一些 buffer
,将新的容量将扩大为原始容量的2倍,也就是10了。
最后,修改 s1
索引为2位置的元素:
s1[2] = 20
这次只会影响原始数组相应位置的元素。它影响不到 s2
了,人家已经远走高飞了。
再提一点,打印 s1
的时候,只会打印出 s1
长度以内的元素。所以,只会打印出3个元素,虽然它的底层数组不止3个元素。
其他
汇编的生成方法
go tool compile -N -S slice.go > slice.S
需要了解unsafe.Pointer 的使用
- slice.go 位于 runtime/slice.go
- 上述代码使用 go1.12.5 版本
- 还有一点需要提醒, type 长度为0的对象。比如说 struct{} 类型。(所以,很多使用chan struct{} 做channel 的传递,节省内存)
package main
import "fmt"
import "unsafe"
func main() {
var data [100000]struct{}
var data1 [100000]int
// 0
fmt.Println(unsafe.Sizeof(data))
// 800000
fmt.Println(unsafe.Sizeof(data1))
}