Go 语言的函数参数传递,只有值传递,没有引用传递
数组与切片
在 Go 中,与 C 数组变量隐式作为指针使用不同,Go 数组是值类型,赋值和函数传参操作都会复制整个数组数据
1 | func main() { |
分析:
- 当直接使用 arrayB = arrayA 进行的是值复制,也就是说Go数组是值类型
- 将数组赋值给函数的时候,也是对整个数组进行复制,函数内改变值并不会改变外面的数组
上面的例子换成切片会怎么样?
1 | func main() { |
分析:
- 当直接使用
arrayB = arrayA
进行的是其实是值复制,地址空间不一致,但是指向的内存空间一致 - 切片做参数传递的时候,但函数内容修改切片内容并不会影响外部切片,改变底层数组会影响外部切片
切片数据结构
切片(slice)切片是一个引用类型,是对数组一个连续片段的引用。这个片段是由起始和终止索引标识的一些项的子集。终止索引标识的项不包括在切片内(左闭右开)。切片提供了一个与指向数组的动态窗口。
给定项的切片索引可能比相关数组的相同元素的索引小。和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度:切片是一个长度可变的数组
1 | type slice struct { |
uintptr
is an integer type that is large enough to hold the bit pattern of any pointer
unsafe.Pointer
通用指针,类似C中的void *
切片创建
首先这里会有一个常见函数,math.MulUintptr
将两个参数相乘来判断是否溢出
1 | //MulUintptr返回a * b以及乘法是否溢出。在受支持的平台上,这是编译器固有的功能。 |
解析:
-
sys.PtrSize
在64位机器中为81
2const PtrSize = 4 << (^uintptr(0) >> 63) // unsafe.Sizeof(uintptr(0)) but an ideal const
const MaxUintptr = ^uintptr(0) -
< 1<<(4*sys.PtrSize)
相当于< 1<<32
,而8位计算机中最大的是2^64-1
,所以判断a和b都小于2^32
即可 -
否则
b > MaxUintptr/a
如果 b 大于最大值除以a,则说明a*b
超过最大值即越界
make与字面量切片
支持通过make或字面量的方式进行创建切片
1 | slice1 := make([]int, 4, 6) //make |
再来看分片函数 makeslice
1 | func makeslice(et *_type, len, cap int) unsafe.Pointer { |
解析:
-
根据
数据类型大小
x切片容量
判断是否会越界 -
如果 越界或超过最大分配长度maxAlloc(32位与64位不一致),长度大于容器。则尝试 根据
数据类型大小
x切片长度
判断溢出 -
如果 长度大于最大长度,则
panicmakeslicelen
: panic报错 makeslice: len out of range -
如果 长度大于容量 则
panicmakeslicecap
: panic报错 makeslice: cap out of range -
否则分配内存:
1
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {}
-
所以切片其实分配了容量大小的内存,只是访问不到,也被初始化
空切片与nil切片
1 | var slice []int //nil切片 |
空切片和 nil 切片的区别在于:空切片指向的地址不是nil,指向的是一个内存地址,但是它没有分配任何内存空间,即底层元素包含0个元素。
不管是使用 nil 切片还是空切片,对其调用内置函数 append,len 和 cap 的效果都是一样的
切片扩容
扩容原理
growslice
用来处理在使用append
时候的切片扩容,那么它的规则如下:
1 | func growslice(et *_type, old slice, cap int) slice { |
上述就是扩容的实现。主要需要关注的有两点:
- 扩容时候的策略
- 如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)
- 如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap)
- 如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的 1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
- 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)
- 扩容是生成全新的内存地址还是在原来的地址后追加
扩容实例
实例1:
1 | func main() { |
分析:
- 1280/1024 = 1.25 , 1696/1024 = 1.325, 2304/1696=1.3584,为什么每次增加的倍数都不一样呢?
- 因为
newcap += newcap / 4
并不是固定倍数,还需要考虑到roundupsize(uintptr(newcap))
- 因为
实例2:
1 | func main() { |
分析:
- 当旧切片容量小于1024,新切片容量直接翻倍
- 这里分配了一个新地址,修改新分片,旧分片并不会改变
实例2:
1 | func main() { |
分析:
- 与实例1进行对比:由于slice还有容量可以扩容,所以执行 append() 操作以后,会在原数组上直接操作,这种情况下,扩容以后的数组还是指向原来的数组
- 实例1中由于没有容量进行扩容,所以执行append之后指向的就是一个全新的数组,修改值并不会影响原来的数组
- 由于数组是值类型,而切片是应引用类型,所以看到
&array
与&array[0]
的地址是一样的。但是&slice
与&slice[0]
的地址是不一样的。另外两者指向的第一个值地址都是一样的,指向同一片内存。
由于容量导致结果不一致,极易产生bug。
实例3:
1 | func main() { |
分析:
切片拷贝
拷贝原理
1 | func slicecopy(toPtr unsafe.Pointer, toLen int, fmPtr unsafe.Pointer, fmLen int, width uintptr) int { |
拷贝实例
实例1:
1 | func main() { |
分析:
- copy返回复制数目,slicecopy 方法最终结果取决于较短的那个切片,当较短的切片复制完成,整个复制过程就全部完成了
实例2:
1 | func main() { |
分析:
- 如果用 range 的方式去遍历一个切片, Value 其实是切片里面的值拷贝。所以每次打印 Value 的地址都不变
- 由于 Value 是值拷贝而非引用传递,所以直接改 Value 是达不到更改原切片值的目的的,需通过
&slice[index]
获取真实的地址