Go杂记1-切片Slice作为函数参数那点事儿

简介: Go杂记1-切片Slice作为函数参数那点事儿

前景提要

今天做项目的时候,最终的结果死活不对。仔细研究之后才发现,原来是踩到slice作为函数参数的一个坑,当时的代码逻辑大概像这样:

func main() {
  input := make([]int, 0)
  fmt.Println("Origianl:", input)
  dealData(input)
  fmt.Println("Output:", input)
}

func dealData(input []int) {
  for i := 0; i < 10; i++ {
    input = append(input, i)
  }
}

首先在函数外部创建了一个切片,而后将切片作为函数参数传入数据处理函数中。在这个函数中,会对原切片进行数据填充。

这种实现的本来目的是获得一个从0到9的切片,结果却是下面这个样子:

也就是说input这个切片进入这个函数没有一点用处。

回过头来想想,我当时之所以要这么写,是因为从某个地方看到“golang的切片可以作为函数参数,等同于引用传递,在函数内修改会影响外部切片”。

我大概知道问题出在哪了,于是写了下面这个函数:

func main() {
  input := make([]int, 10)
  fmt.Println("Origianl:", input)
  dealData(input)
  fmt.Println("Output:", input)
}

func dealData(input []int) {
  for i := 0; i < 10; i++ {
    input[i] = i
  }
}

这个时候的输出就是:

那么这到底是什么问题呢?为什么我可以修改原切片元素,但是append就不行?

问题解析

其实简单来说,如果只是进行元素的修改,将切片作为参数没有一点问题,但只在函数中对原切片进行了数据的增加等操作,就会造成上述的现象。

归根究底,还是得说会golang切片(slice)的底层结构:

type slice struct {
  array unsafe.Pointer
  len   int
  cap   int
}

切片其实主要的构成元素有三个:指向底层数组的指针ptr、当前数组中元素个数len、底层数组可容纳的最大元素个数cap。

当我们将切片作为函数形参传递时,其实传递的就是这三个主要元素,由于形参指向的底层数组地址和外部切片指向的底层数组相同,因此在函数内部对切片进行的修改都会反应到外部。

但是,当我们进行append操作的时候,情况就不一样了。

**append函数对原切片进行填充,如果原切片容量足够,没有达到扩容的阈值,就在原切片的底层数组上进行数组填充;如果切片容量不够,就进行扩容,并将原数据复制过去再进行数据填充。而后者就会让原切片的底层数组地址发生变动,**这也是为什么,append函数返回的也是一个切片,而且一般使用它的时候都是这样的:

arr = append(arr, "h")

这是从语法层面防止用户遗忘了这个特性。

让我们来看一组代码:

func main() {
  input := make([]int, 0)
  fmt.Println("Origianl:", input)
  fmt.Printf("Origianl address %p   %p;\n", &input, input)
  dealData(input)
  fmt.Println("Output:", input)
  fmt.Printf("Output address %p   %p;\n", &input, input)
}

func dealData(input []int) {
  for i := 0; i < 10; i++ {
    input = append(input, i)
    fmt.Printf("i = %d ,len = %d ,cap = %d ,Temp address is %p   %p\n", i, len(input), cap(input), &input, input)
  }
}

其中 fmt.Printf(“Origianl address %p %p;\n”, &input, input) 这一行代码会先打出切片的内存地址,再打出切片的底层数组地址。

好好看看这组代码的输出:

Origianl: []
Origianl address 0xc000004078   0xfc2438;
i = 0 ,len = 1 ,cap = 1 ,Temp address is 0xc0000040c0   0xc000012098//第一次扩容
i = 1 ,len = 2 ,cap = 2 ,Temp address is 0xc0000040c0   0xc0000120d0//第二次
i = 2 ,len = 3 ,cap = 4 ,Temp address is 0xc0000040c0   0xc00000a2a0
i = 3 ,len = 4 ,cap = 4 ,Temp address is 0xc0000040c0   0xc00000a2a0
i = 4 ,len = 5 ,cap = 8 ,Temp address is 0xc0000040c0   0xc000010280/第三次
i = 5 ,len = 6 ,cap = 8 ,Temp address is 0xc0000040c0   0xc000010280
i = 6 ,len = 7 ,cap = 8 ,Temp address is 0xc0000040c0   0xc000010280
i = 7 ,len = 8 ,cap = 8 ,Temp address is 0xc0000040c0   0xc000010280
i = 8 ,len = 9 ,cap = 16 ,Temp address is 0xc0000040c0   0xc00001a180//第四次
i = 9 ,len = 10 ,cap = 16 ,Temp address is 0xc0000040c0   0xc00001a180
Output: []
Output address 0xc000004078   0xfc2438;

我想大家已经发现了吧?

从进入函数之后append第一个元素之后,切片的底层数组地址就已经跟函数外部切片的底层数组地址不一致了!

这是因为append导致了扩容,并且这种扩容现象在之后发生了几次,从而造成底层数组地址持续变动。

总结

虽然踩了坑,但是对切片的理解也更加深入了。

回到题目,如果想解决前景提要中遇到的问题应该怎么办?

有两种简单的方式:一是传入切片指针,而不是切片本身;二是让函数返回一个切片,对外部切片进行赋值,而不是直接将外部切片作为参数传递进去

如果非要传入切片,那就在外部申请足够大空间,避免底层数组地址的变动,但是这个方法实际长场景不好操作,很多时候并不知道需要申请多大的空间。

推荐阅读

https://halfrost.com/go_slice/

,而不是直接将外部切片作为参数传递进去**。

如果非要传入切片,那就在外部申请足够大空间,避免底层数组地址的变动,但是这个方法实际长场景不好操作,很多时候并不知道需要申请多大的空间。

推荐阅读

https://halfrost.com/go_slice/

https://blog.csdn.net/lengyue1084/article/details/108124045

相关文章
|
27天前
|
存储 算法 Go
go语言中的延迟执行函数
【5月更文挑战第13天】`defer`是Go语言中用于延迟执行函数的关键字,尤其适用于资源管理,如文件关闭和锁的释放。它在函数返回前按照LIFO顺序执行,确保资源在任何返回路径下都能正确释放。`defer`可以拦截`panic`并在函数返回前执行,但无法阻止某些致命的`panic`。此外,`defer`可用于修改返回值、输出调试信息和还原变量值。尽管在某些场景下可能影响性能,但Go的优化使得其在多数情况下性能表现良好,特别是在资源清理方面。在Go 1.20及以后的版本,`defer`的性能已显著提升,尤其是在高计算量的场景下。
247 2
|
1天前
|
存储 监控 Go
【Go语言精进之路】构建高效Go程序:了解切片实现原理并高效使用
【Go语言精进之路】构建高效Go程序:了解切片实现原理并高效使用
16 3
|
7天前
|
JSON Go 数据格式
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】(4)
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】
|
7天前
|
Java 编译器 Go
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】(3)
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】
|
7天前
|
存储 安全 Go
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】(2)
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】
|
7天前
|
Java Go 索引
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】(1)
Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】
|
19天前
|
Go 索引
Go 语言切片(Slice)
Go 语言切片(Slice)
16 1
|
20天前
|
编译器 Go
Go 语言函数
Go 语言函数
16 7
|
21小时前
|
算法 编译器 Go
Go语言中,常量和变量的定义和使用
Go语言中,常量和变量的定义和使用