柚子快报激活码778899分享:golang go切片实现原理

http://yzkb.51969.com/

近日一直在学习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时候不在使用二倍扩容,转而使用其他算法)

柚子快报激活码778899分享:golang go切片实现原理

http://yzkb.51969.com/

好文链接

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。