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

相关文章
|
3月前
|
测试技术 Go API
Go 切片导致 rand.Shuffle 产生重复数据的原因与解决方案
在 Go 语言开发中,使用切片时由于其底层数据共享特性,可能会引发意想不到的 Bug。本文分析了 `rand.Shuffle` 后切片数据重复的问题,指出原因在于切片是引用类型,直接赋值会导致底层数组共享,进而影响原始数据。解决方案是使用 `append` 进行数据拷贝,确保独立副本,避免 `rand.Shuffle` 影响原始数据。总结强调了切片作为引用类型的特性及正确处理方法,确保代码稳定性和正确性。
139 82
|
5月前
|
存储 Go 索引
go语言中数组和切片
go语言中数组和切片
89 7
|
6月前
|
Go 索引
Go语言中,遍历数组或切片
在Go语言中,遍历数组或切片
137 6
|
3月前
|
存储 Go
Go 语言入门指南:切片
Golang中的切片(Slice)是基于数组的动态序列,支持变长操作。它由指针、长度和容量三部分组成,底层引用一个连续的数组片段。切片提供灵活的增减元素功能,语法形式为`[]T`,其中T为元素类型。相比固定长度的数组,切片更常用,允许动态调整大小,并且多个切片可以共享同一底层数组。通过内置的`make`函数可创建指定长度和容量的切片。需要注意的是,切片不能直接比较,只能与`nil`比较,且空切片的长度为0。
Go 语言入门指南:切片
|
5月前
|
Go 索引
go语言for遍历数组或切片
go语言for遍历数组或切片
192 62
|
6月前
|
Go 索引
go语言遍历数组和切片
go语言遍历数组和切片
43 2
|
5月前
|
Go 数据安全/隐私保护 UED
优化Go语言中的网络连接:设置代理超时参数
优化Go语言中的网络连接:设置代理超时参数
|
6月前
|
JSON 安全 网络协议
go语言使用内置函数和标准库
【10月更文挑战第18天】
59 3
|
6月前
|
存储 Go
|
6月前
|
Java Go 数据处理
go语言使用切片而非数组
【10月更文挑战第18天】
37 1

热门文章

最新文章