Golang slice源码分析
Posted 非典型程序员
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang slice源码分析相关的知识,希望对你有一定的参考价值。
蓝字
slice数据结构定义
type slice struct {
array unsafe.Pointer
len int
cap int
}
slice底层存储实现为数组。
slice结构体有三个字段
array:是一个 unsafe.Pointer 非安全类型的指针,指向一个底层数组,是一块连续的内存块
len:指slice的实际长度,即slice中含有的元素个数
cap:指slice底层数组的长度,即slice可以容纳多少个元素
按上面的说法slice的直接值部只含有三个int型字段,也就是slice对象的大小为3个word(机器码),也就是和机器的位数是相关的。
在64位机器上,slice对象大小是3*8byte(64位)=24byte,在32位机器上是3*4byte(32位)=12byte。
slice的大小和slice中含有的元素个数是无关的,仅仅是通过指针来关联底层数组的。
示例:slice对象大小
func Test(t *testing.T) {
var slice1 = make([]int64, 0, 10)
t.Log(unsafe.Sizeof(slice1)) //24
slice1 = append(slice1, 1)
slice1 = append(slice1, 2)
slice1 = append(slice1, 3)
t.Log(unsafe.Sizeof(slice1)) //24
}
makeslice方法-源码分析
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()
}
/*
分配一个大小为bytes的对象。
小对象是从每个p缓存的空闲列表中分配的。
大对象(> 32kb)直接从堆中分配。
*/
return mallocgc(mem, et, true)
}
growslice方法-源码分析
1. 原切片cap<1024,扩容为原来cap的2倍
func growslice(et *_type, old slice, cap int) slice {
//....
//....
/*
如果新的cap<原cap,那么panic
*/
if cap < old.cap {
panic(errorString("growslice: cap out of range"))
}
/*
若是slice元素长度为为0
像 []struct{},每个元素的长度是空的
那么返回新的slice对象中的array大小仍然保持0,
len为原len,cap为新cap,进行简单的扩容,其它不做任何内存分配的操作
*/
if et.size == 0 {
return slice{unsafe.Pointer(&zerobase), old.len, cap}
}
//默认设置目标容量为原容量
newcap := old.cap
//求出二倍原容量大小
doublecap := newcap + newcap
if cap > doublecap {
/*
如果入参的容量>两倍原容量
那么目标容量为2部原容量
*/
newcap = cap
} else {
/*
入参容量<2倍原容量的情况下
原容量<1024时,目标容量为原容量的2倍
*/
if old.cap < 1024 {
newcap = doublecap
} else {
//原容量 >= 1024 && 原容量<入参容量的时候,目标在原容量的基础上扩容25%
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
//如果原容量为0,那么目标容量为入参的容量
if newcap <= 0 {
newcap = cap
}
}
}
//....
//跟据切片类型和容量计算要分配内存的大小
//....
//判断内存是否溢出
if overflow || capmem > maxAlloc {
panic(errorString("growslice: cap out of range"))
}
//内存分配,为新的切片开辟容量为capmem的地址空间
var p unsafe.Pointer
if et.ptrdata == 0 {
p = mallocgc(capmem, nil, false)
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
p = mallocgc(capmem, et, true)
if lenmem > 0 && writeBarrier.enabled {
bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem-et.size+et.ptrdata)
}
}
//数据拷贝
memmove(p, old.array, lenmem)
return slice{p, old.len, newcap}
}
slicecopy方法-源码分析
slice深拷贝的实现
func slicecopy(toPtr unsafe.Pointer, toLen int, fromPtr unsafe.Pointer, fromLen int, width uintptr) int {
if fromLen == 0 || toLen == 0 {
return 0
}
n := fromLen
if toLen < n {
n = toLen
}
if width == 0 {
return n
}
//...
//竞态剖析&内存扫描
//...
//内存深拷贝
if size == 1 {
//原到原地址指针取值再设值,完成一次内存深度拷贝
*(*byte)(toPtr) = *(*byte)(fromPtr)
} else {
memmove(toPtr, fromPtr, size)
}
return n
}
-
如果不关注数据结构里面的具体值的话,使用struct{}可以节约内存,很多数据结构针对此做了优化。比如map[int]struct{} 、[]struct{}等。 -
如果一开始就能知道slice的大小,尽量预设置切片的容量。避免自动扩容的开销 -
切片作为函数入参时,会进行值拷贝,另外slice中对底层数组是通过指针引用,实际仍然会共享底层数据,仅仅是进行了切片直接值部的拷贝。
使用实践及注意点
01
使用append进行元素添加的时候,需要将append返回值赋值给原切片
进行append的时候,可能会导致切片的len和cap大小的变化。一定要将append方法的返回值赋值给原切片才正确。
给切片append元素有两种情况:
1. 如果原切片容量足够,即无需扩容的情况下,仅仅是len发生了变化。
2. 如果原切片容量不够,那么切片底层数据会出现扩容的情况,len和cap都将发生变化。
如果忽略掉append方法的返回值,则会察觉不到切片的变化。
func Test(t *testing.T) {
var s1 = make([]int, 0, 10) //长度为0,容量为10
_ = append(s1,1)//错误的用法,忽略append的返回值
t.Log(s1)//这里输出为空[]这里无法看到append后的元素,不符合我们的预期
}
func Test(t *testing.T) {
var s1 = make([]int, 5, 10) //长度为5,容量为10,确保增加一个元素后切片不会被扩容
t.Log(s1)//[0 0 0 0 0]
t.Logf("s1-append-before len=%v cap=%v add=%p", len(s1), cap(s1), &s1[0])//s1-append-before len=5 cap=10 add=0xc000022140
_ = append(s1, 1)//忽略append函数的返回值
/*
这里发发现,因为没有接收append方法的返回值,append一个元素后,切片的长度并没有增加
*/
t.Logf("s1-append-after after len=%v cap=%v add=%p", len(s1), cap(s1), &s1[0])//s1-append-after after len=5 cap=10 add=0xc000022140
reflect.ValueOf(&s1).Elem().SetLen(len(s1)+1)//通过反射人为将len增加1
t.Logf("s1-append-after2 len=%v cap=%v add=%p", len(s1), cap(s1), &s1[0])//s1-append-after2 len=6 cap=10 add=0xc000022140
t.Log(s1)//[0 0 0 0 0 1] 这里已经看到我们append的元素了,符合我们的预期了
}
02
切片的容量和len可以通过反射人为修改
func Test(t *testing.T) {
s := make([]int, 2, 6)
fmt.Println(len(s), cap(s)) // 2 6
reflect.ValueOf(&s).Elem().SetLen(3)//范围[0-6(cap)]超出范围panic
fmt.Println(len(s), cap(s)) // 3 6
reflect.ValueOf(&s).Elem().SetCap(7)//范围[3(len+1)-6(cap)]超出范围panic
fmt.Println(len(s), cap(s)) // 3 5
}
03
切片的子切片和原切片使用同一个底层数组,修改子切片对原切片可见
func Test(t *testing.T) {
slice := []int{0,1,2,3,4,5}
/*
从原切片中解脱第3,4个元素
*/
subSlice :=slice[2:4]//[2 3]
t.Log(subSlice)//[2 3]
subSlice[1]=888//通过子切片改值
t.Log(subSlice)//[2 888]
t.Log(slice)//[0 1 2 888 4 5] 在原切片中也会生效
}
04
切片的0值为nil,对nil切片进行append不会panic
func Test8(t *testing.T) {
var slice []int
t.Log(slice==nil)//true
slice = append(slice,1)//不会panic
t.Log(slice)//1
}
05
删除一个切片元素
// 第一种方法(保持剩余元素的次序):
s = append(s[:i], s[i+1:]...)
// 第二种方法(保持剩余元素的次序):
s = s[:i + copy(s[i:], s[i+1:])]
// 上面两种方法都需要复制len(s)-i-1个元素。
// 第三种方法(不保持剩余元素的次序):
s[i] = s[len(s)-1]
s = s[:len(s)-1]
06
删除一段切片元素
// 第一种方法(保持剩余元素的次序):
s = append(s[:from], s[to:]...)
// 第二种方法(保持剩余元素的次序):
s = s[:from + copy(s[from:], s[to:])]
// 第三种方法(不保持剩余元素的次序):
if n := to-from; len(s)-to < n {
copy(s[from:to], s[to:])
} else {
copy(s[from:to], s[len(s)-n:])
}
s = s[:len(s)-(to-from)]
大家可以随手点击个关注支持一下哈
以上是关于Golang slice源码分析的主要内容,如果未能解决你的问题,请参考以下文章