近日一直在学习golang,已经产出如下博客一篇
- GO闭包实现原理(汇编级讲解)
引言
最近在使用go语言的切片时,出现了一些意料之外的情况,遂查询相关文档学习后写下此篇博客
正文
首先,我们思考,go在通过函数传递一个切片
时,是通过引用传递
的吗,还是通过值传递
的呢(答案将会很意外的哦)
值传递?
首先,先看如下简单代码,将一个string类型的切片传入函数后经过修改,在使用append()
函数对切片进行添加之后,在函数的外部进行打印后却能发现,在内部添加数据并没有影响function()
函数外面的str
看起来像是值传递,让我们继续往下看
func function(str []string){
str = append(str,"c","lua","c#")
}
func main() {
str := []string{"c++","java","golang"}
function(str)
fmt.Println(str)
}
[Running] go run "d:\goProject\src\learn\package main.go"
[c++ java golang]
[Done] exited with code=0 in 1.586 seconds
引用传递?
将一个string类型的切片传入函数后经过修改,修改后影响到了外面[]string切片
func function(str []string){
str[1] = "python"
}
func main() {
str := []string{"c++","java","golang"}
function(str)
fmt.Println(str)
}
[Running] go run "d:\goProject\src\learn\package main.go"
[c++ python golang]
所以,go的切片是使用的引用传递吗?
no,请继续向下看
我们可以惊奇的发现,先对切片进行append
追加,在进行修改后,在函数外面进行打印,修改居然失效了
func function(str []string){
str = append(str,"c","lua","c#")
str[1] = "python"
}
func main() {
str := []string{"c++","java","golang"}
function(str)
fmt.Println(str)
}
[Running] go run "d:\goProject\src\learn\package main.go"
[c++ java golang]
但如果我们先用make()
函数先对[]string
切片的容量进行设置,在进行赋值又能发现是有效的
func function(str []string) {
str = append(str, "c", "lua", "c#")
str[1] = "python"
}
func main() {
str := make([]string,3,10)
str1 := []string{"c++", "java", "golang"}
copy(str,str1)
function(str)
fmt.Print(str)
}
[Running] go run "d:\goProject\src\learn\package main.go"
[c++ python golang]
[Done] exited with code=0 in 1.858 seconds
到这里,读者的cpu是不是已经麻成一团了呢,哈哈,请先让我先对其中切片的原理进行讲解后,再回头来看,相信一定能看懂
原理解析
首先,切片类型
在编译时期会生成一个结构体
array
:相当于一个c语言的数组指针,指向切片的实际内存区域len
:切片的实际使用大小cap
:切片当前能够容纳的最大数量
type splice struct {
array unsafe.Pointer
len int
cap int
}
如下图所示
而在我们将切片通过函数传入时候,go直接对这个结构体进行了一次拷贝,也就是说,拷贝的是这个结构体的值,而不是真正的数组,如下图所示,拷贝是一次浅拷贝,两个结构体指针指向同一个底层的数组
所以,当我们没有使用make()
函数生成切片类型,并且设置切片的cap
容量时候:
go就会对底层的数组进行一次扩容,此时传入函数的切片的array
就会指向一块新的内存,如下图所示,故修改无用
然而,如果我们使用make()
生成切片,并且设置了cap
,那么就会发生如下事情
假设len==3
,cap==10
- 切片传入函数后添加3条数据,此时函数内的切片
len==6
,cap==10
- 由于传入时候,传入的
splice
结构体是值传递,所以,函数外的splice
结构体len==3
,cap==10
,也就是说len
变量并没有被修改,但是对于[0,len]
这个区间内的参数的修改是可见的,然而,由于go有着比较严格的内存安全检查,如果我们直接对[3,6]
这个区间的内存进行访问,go会提示运行时错误
实测
接下来我们进行实测,通过一点类似于c语言指针的骚操作
,绕过go的安全检查,验证我们理论的正确性
- 首先使用
make()
生成切片,设置len==3
,cap==10
- 使用
copy()
函数,将切片前三个string变量进行赋值 - 将切片通过函数传递给
function()
函数,在function()
函数内部进行追加以及修改 function()
函数返回后,通过类似于c语言指针的骚操作
,绕过go的安全检查,访问到len[3:6]
这个区间的内容- 通过打印可以看见,如我们所想,在
function()
函数内的修改和添加都成功了
func function(str []string) {
str = append(str, "c", "lua", "c#")
str[1] = "python"
}
func main() {
str := make([]string,3,10)
str1 := []string{"c++", "java", "golang"}
copy(str,str1)
function(str)
//fmt.Print(str)
for i := 0; i < 6; i++ {
ptr := unsafe.Pointer(uintptr(unsafe.Pointer(&str[0])) + uintptr(i)*unsafe.Sizeof(str[0]))
fmt.Printf("%s,", *(*string)(unsafe.Pointer(ptr)))
}
}
[Running] go run "d:\goProject\src\learn\package main.go"
c++,python,golang,c,lua,c#,
[Done] exited with code=0 in 1.757 seconds
总结
- 切片在底层是一个结构体,在进行赋值传递时候,是将该结构体进行浅拷贝
- 切片就是相当于一个动态数组,容量足够时候直接添加,不够时候重新创建一个更大的数组,再将原本的数据移动到新的数组(经过个人测试:默认二倍扩容,大小超过512时候不在使用二倍扩容,转而使用其他算法)