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

相关文章
|
4月前
|
存储 JSON 安全
Go语言切片,使用技巧与避坑指南
Go语言中的切片(Slice)是动态引用数组的高效数据结构,支持扩容与截取。本文从切片基础、常用操作到高级技巧全面解析,涵盖创建方式、`append`扩容机制、共享陷阱及安全复制等内容。通过代码示例详解切片特性,如预分配优化性能、区分`nil`与空切片、处理多维切片等。掌握这些核心知识点,可编写更高效的Go代码。
169 2
|
21天前
|
存储 Java Go
对比Java学习Go——函数、集合和OOP
Go语言的函数支持声明与调用,具备多返回值、命名返回值等特性,结合`func`关键字与类型后置语法,使函数定义简洁直观。函数可作为一等公民传递、赋值或作为参数,支持匿名函数与闭包。Go通过组合与接口实现面向对象编程,结构体定义数据,方法定义行为,接口实现多态,体现了Go语言的简洁与高效设计。
|
3月前
|
数据采集 机器学习/深度学习 存储
Go语言实战案例 - 找出切片中的最大值与最小值
本案例通过实现查找整数切片中的最大值与最小值,帮助初学者掌握遍历、比较和错误处理技巧,内容涵盖算法基础、应用场景及完整代码示例,适合初学者提升编程能力。
|
4月前
|
开发框架 JSON 中间件
Go语言Web开发框架实践:路由、中间件、参数校验
Gin框架以其极简风格、强大路由管理、灵活中间件机制及参数绑定校验系统著称。本文详解其核心功能:1) 路由管理,支持分组与路径参数;2) 中间件机制,实现全局与局部控制;3) 参数绑定,涵盖多种来源;4) 结构体绑定与字段校验,确保数据合法性;5) 自定义校验器扩展功能;6) 统一错误处理提升用户体验。Gin以清晰模块化、流程可控及自动化校验等优势,成为开发者的优选工具。
|
4月前
|
Go 索引
Go语言中使用切片需要注意什么?
本文详细讲解了Go语言中切片(Slice)的使用方法与注意事项。切片是对数组连续片段的引用,具有灵活的操作方式。文章从定义与初始化、长度与容量、自动扩容、共享底层数组、复制、边界检查、零值到拼接等方面展开,并配以示例代码演示。通过学习,读者可深入了解切片的工作原理及优化技巧,避免常见陷阱,提升编程效率与代码质量。
115 2
|
4月前
|
人工智能 Dart Go
Go语言中的make和new函数的区别及使用场景
本文详细解析了Go语言中`make`和`new`函数的使用方法及区别。`make`用于创建切片、映射和通道等引用类型,返回初始化后的值;`new`用于创建任意类型的零值对象,返回指向该对象的指针。文章通过多个示例说明两者的应用场景,并总结了面试中可能遇到的相关问题,如底层实现、使用场景及优缺点等,帮助读者更好地理解和区分这两个函数。
124 1
|
5月前
|
Go 调度
GO语言函数的内部运行机制分析
以上就是Go语言中函数的内部运行机制的概述,展示了函数在Go语言编程中如何发挥作用,以及Go如何使用简洁高效的设计,使得代码更简单,更有逻辑性,更易于理解和维护。尽管这些内容深入了一些底层的概念,但我希望通过这种方式,将这些理论知识更生动、更形象地带给你,让你在理解的同时找到编程的乐趣。
78 5
|
5月前
|
人工智能 Go
[go]Slice 切片原理
本文详细介绍了Go语言中的切片(slice)数据结构,包括其定义、创建方式、扩容机制及常见操作。切片是一种动态数组,依托底层数组实现,具有灵活的扩容和传递特性。文章解析了切片的内部结构(包含指向底层数组的指针、长度和容量),并探讨了通过`make`创建切片、基于数组生成切片以及切片扩容的规则。此外,还分析了`append`函数的工作原理及其可能引发的扩容问题,以及切片拷贝时需要注意的细节。最后,通过典型面试题深入讲解了切片在函数间传递时的行为特点,帮助读者更好地理解和使用Go语言中的切片。
120 0
|
8月前
|
编译器 Go
揭秘 Go 语言中空结构体的强大用法
Go 语言中的空结构体 `struct{}` 不包含任何字段,不占用内存空间。它在实际编程中有多种典型用法:1) 结合 map 实现集合(set)类型;2) 与 channel 搭配用于信号通知;3) 申请超大容量的 Slice 和 Array 以节省内存;4) 作为接口实现时明确表示不关注值。此外,需要注意的是,空结构体作为字段时可能会因内存对齐原因占用额外空间。建议将空结构体放在外层结构体的第一个字段以优化内存使用。