go切片实现原理

近日一直在学习golang,已经产出如下博客一篇

引言

最近在使用go语言的切片时,出现了一些意料之外的情况,遂查询相关文档学习后写下此篇博客

正文

首先,我们思考,go在通过函数传递一个切片时,是通过引用传递的吗,还是通过值传递的呢(答案将会很意外的哦)

值传递?

首先,先看如下简单代码,将一个string类型的切片传入函数后经过修改,在使用append()函数对切片进行添加之后,在函数的外部进行打印后却能发现,在内部添加数据并没有影响function()函数外面的str

看起来像是值传递,让我们继续往下看

go 复制代码
func function(str []string){
	str = append(str,"c","lua","c#")
}


func main() {
	str := []string{"c++","java","golang"}
	function(str)
	fmt.Println(str)
}
shell 复制代码
[Running] go run "d:\goProject\src\learn\package main.go"
[c++ java golang]

[Done] exited with code=0 in 1.586 seconds

引用传递?

将一个string类型的切片传入函数后经过修改,修改后影响到了外面[]string切片

go 复制代码
func function(str []string){
	str[1] = "python"
}

func main() {
	str := []string{"c++","java","golang"}
	function(str)
	fmt.Println(str)
}
shell 复制代码
[Running] go run "d:\goProject\src\learn\package main.go"
[c++ python golang]

所以,go的切片是使用的引用传递吗?

no,请继续向下看

我们可以惊奇的发现,先对切片进行append追加,在进行修改后,在函数外面进行打印,修改居然失效了

go 复制代码
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)
}
shell 复制代码
[Running] go run "d:\goProject\src\learn\package main.go"
[c++ java golang]

但如果我们先用make()函数先对[]string切片的容量进行设置,在进行赋值又能发现是有效的

go 复制代码
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)
}
shell 复制代码
[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:切片当前能够容纳的最大数量
go 复制代码
type splice struct {
	array    unsafe.Pointer
	len      int
	cap		 int
}

如下图所示

而在我们将切片通过函数传入时候,go直接对这个结构体进行了一次拷贝,也就是说,拷贝的是这个结构体的值,而不是真正的数组,如下图所示,拷贝是一次浅拷贝,两个结构体指针指向同一个底层的数组

所以,当我们没有使用make()函数生成切片类型,并且设置切片的cap容量时候:

go就会对底层的数组进行一次扩容,此时传入函数的切片的array就会指向一块新的内存,如下图所示,故修改无用

然而,如果我们使用make()生成切片,并且设置了cap,那么就会发生如下事情

假设len==3,cap==10

  1. 切片传入函数后添加3条数据,此时函数内的切片len==6,cap==10
  2. 由于传入时候,传入的splice结构体是值传递,所以,函数外的splice结构体len==3,cap==10,也就是说len变量并没有被修改,但是对于[0,len]这个区间内的参数的修改是可见的,然而,由于go有着比较严格的内存安全检查,如果我们直接对[3,6]这个区间的内存进行访问,go会提示运行时错误

实测

接下来我们进行实测,通过一点类似于c语言指针的骚操作 ,绕过go的安全检查,验证我们理论的正确性

  1. 首先使用make()生成切片,设置len==3,cap==10
  2. 使用copy()函数,将切片前三个string变量进行赋值
  3. 将切片通过函数传递给function()函数,在function()函数内部进行追加以及修改
  4. function()函数返回后,通过类似于c语言指针的骚操作,绕过go的安全检查,访问到len[3:6]这个区间的内容
  5. 通过打印可以看见,如我们所想,在function()函数内的修改和添加都成功了
go 复制代码
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)))
    }
}
shell 复制代码
[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时候不在使用二倍扩容,转而使用其他算法)
相关推荐
qq_172805594 小时前
GIN 反向代理功能
后端·golang·go
__AtYou__5 小时前
Golang | Leetcode Golang题解之第535题TinyURL的加密与解密
leetcode·golang·题解
kevin_tech9 小时前
Go API 多种响应的规范化处理和简化策略
开发语言·后端·golang·状态模式
幺零九零零12 小时前
【Golang】sql.Null* 类型使用(处理空值和零值)
数据库·sql·golang
cookies_s_s13 小时前
Golang--DOS命令、变量、基本数据类型、标识符
golang
__AtYou__13 小时前
Golang | Leetcode Golang题解之第541题反转字符串II
leetcode·golang·题解
flying robot13 小时前
Go的JSON转化
golang
幺零九零零21 小时前
【Golang】validator库的使用
开发语言·后端·golang
海绵宝宝de派小星1 天前
Go:接口和反射
开发语言·后端·golang
techdashen1 天前
Go Modules和 雅典项目
开发语言·后端·golang