作者:林冠宏 / 指尖下的幽灵。转载者,请: 务必标明出处。

GitHub : https://github.com/af913337456/

出版的书籍:


  • 例子

    • 切片作为函数参数传递的是值
    • 用来误导切片作为函数参数传递的是引用
    • 函数内切片 append 引起扩容的修改将无效
    • 不引起切片底层数组扩容,验证没指向新数组
    • 脚踏实地让切片在函数内的修改
  • 彩蛋

切片 slice 几乎是每个 Go 开发者以及项目中 100% 会高频使用到的,Go 语言的知识很广,唯独 slice 我个人认为是必须要深入了解的。

乃至于今,网上还有很多关于切片 slice 技术文章一直存在的错误内容:切片作为函数参数传递的是引用,这是错误的。

无论是官方说明还是实践操作都表明:切片作为函数参数传递的是值,和数组一样。


接下来我们直接看例子以加深印象。

切片作为函数参数传递的是值的例子:
func main() {
mSlice := []int{1, 2, 3}
fmt.Printf("main-1: %p \n", &mSlice) // 0x140000b2000
mAppend(mSlice)
fmt.Printf("main-2: %p \n", &mSlice) // 0x140000b2000
} func mAppend(slice []int) {
fmt.Printf("append func: %p \n", &slice) // 0x140000b2018 和外部的不一样
}
错觉例子,也是现在用来误导切片作为函数参数传递的是引用的错误文章常用的
func main() {
mSlice := []int{1, 2, 3}
fmt.Printf("main-1: %v \n", mSlice) // [1,2,3]
mAppend(mSlice)
fmt.Printf("main-2: %v \n", mSlice) // [1,9,3],这里2被修改了,但不是引用传递导致的
} func mAppend(slice []int) {
slice[2] = 9 // 修改
}
切片的内部结构:
// 源码路径:go/src/runtime/slice.go
type slice struct {
array unsafe.Pointer // 指针
len int
cap int
}

切片的本质是 struct,作为函数参数传递时候遵循 struct 性质,array 是指针指向一个数组,len 是数组的元素个数,cap 是数组的的长度。当 len > cap,将触发数组扩容。

解析: 为什么上面的 错觉例子 能在函数内部改变值且在外部生效。这是因为当切片作为参数传递到函数里,虽然是值传递,但函数内拷贝出的新切片的 array 指针所指向的数组和外部的旧切片是一样的,那么在没引起扩容情况下进行值的修改就生效了。

旧切片 array 指针 ---> 数组-1

新切片 array 指针 ---> 数组-1,函数内发生改变


函数内切片 append 引起扩容的修改将无效的例子:
func main() {
mSlice := []int{1, 2, 3}
fmt.Printf("main-1: %v \n", mSlice) // [1,2,3]
mAppend(mSlice)
fmt.Printf("main-2: %v \n", mSlice) // [1,2,3] 没生效
} func mAppend(slice []int) {
// slice[2] = 9 // 修改
slice = append(slice, 4)
fmt.Printf("append: %v \n", slice) // [1,2,3,4]
}

解析:切片初始化时候添加了3个数,导致其 len 和 cap 都是3,函数内添加第四个数的时候,触发扩容,而扩容会导致扩容,array 指针指向新的数组,在函数结束后,旧切片数组并没修改。

旧切片 array 指针 ---> 数组-1 值 [1,2,3]

新切片 array 指针 ---> 数组-2 值 [1,2,3,4]


不引起切片底层数组扩容,验证没指向新数组例子:
func main() {
mSlice := make([]int, 3, 4) // len = 3, cap = 4, cap > len
fmt.Printf("main-1: %v, 数组地址: %p \n", mSlice, mSlice) // [0,0,0], 0x14000120000
mAppend(mSlice)
fmt.Printf("main-2: %v, 数组地址: %p \n", mSlice, mSlice) // [0,0,0], 0x14000120000
} func mAppend(slice []int) {
slice = append(slice, 4)
fmt.Printf("append: %v, 数组地址: %p \n", slice, slice) // [0,0,0,4], 0x14000120000
}

解析:可以看到切片的底层数组地址并没改变,但是数组的值依然没改变。这是因为切片是值传递到函数内部的,此时的 len 依然是值传递,当打印的时候,就只打印 len 以内的数据。

旧切片 len = 3

新切片 len = 4,函数内改变


至此,我们应该如何让切片在函数内的修改生效?答案就是规规矩矩使用指针传参

func main() {
mSlice := []int{1, 2, 3}
fmt.Printf("main-1: %v, 数组地址: %p \n", mSlice, mSlice) // [1,2,3], 0x1400001a0a8
mAppend(&mSlice)
fmt.Printf("main-2: %v, 数组地址: %p \n", mSlice, mSlice) // [1,2,3,4], 0x1400001a0a8
} func mAppend(slice *[]int) {
*slice = append(*slice, 4)
fmt.Printf("append: %v, 数组地址: %p \n", *slice, slice) // [1,2,3,4], 0x140000181b0
}

上面例子成功在函数内使用 append 修改了切片,也可以看到切片数组地址变了,这是因为引起了扩容。但 array 指针没变,所以扩容后,指向了新的。

切片 array 指针 ---> 数组-1 值 [1,2,3]

切片 array 指针 ---> 数组-2 值 [1,2,3,4]

彩蛋

切片的扩容:

  • go1.18之前,临界值为1024,len 小于1024时,切片先2倍 len 扩容。大于 1024,每次增加 25% 的容量,直到新容量大于期望容量;

  • go1.18之后,临界值为256,len 小于256,依然2倍 len 扩容。大于256走算法:newcap += (newcap + 3*threshold) / 4,直到满足。(threshold = 256)

Golang 切片作为函数参数传递的陷阱与解答的更多相关文章

  1. golang 切片小记

    1 切片初始化 func printSlice(s []int) { fmt.Printf("len=%d cap=%d underlying array:%p, %v\n", l ...

  2. Golang基础之函数

    golang基础之函数 1.为什么需要函数? 有些相同的代码可能出现多次,如果不进行封装,那么多次写入到程序中,会造成程序冗余,并且可读性降低 2.什么是函数 为完成某些特定功能的程序指令集合称为函数 ...

  3. Golang面向过程编程-函数

    Golang面向过程编程-函数 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.什么是函数 简单的说函数的作用就是把程序里多次调用的相同的代码部分定义成一份,然后起个名字,所有的 ...

  4. shell 脚本之获取命令输出字符串以及函数参数传递

    在ubuntu 14.04之后,所有的U盘挂载也分用户之分,最近很多操作也和U盘有关,所以就研究了一上午shell脚本函数以及字符串操作的方法. 字符串操作: 获取他的命令输出比较简单,打个简单的比方 ...

  5. Python语言特性之1:函数参数传递

    问题:在Python文档中好像没有明确指出一个函数参数传递的是值传递还是引用传递.如下面的代码中"原始值"是不放生变化的: class PassByReference: def _ ...

  6. Go的50度灰:Golang新开发者要注意的陷阱和常见错误

    Go的50度灰:Golang新开发者要注意的陷阱和常见错误 http://colobu.com/2015/09/07/gotchas-and-common-mistakes-in-go-golang/

  7. VB几种函数参数传递方法,Variant,数组,Optional,ParamArray

    VB几种函数参数传递方法,Variant,数组,Optional,ParamArray 一) 过程的参数被缺省为具有 Variant 数据类型. 1)ByRef按 地址传递参数在 VB 中是缺省的 按 ...

  8. Python 函数参数传递机制.

    learning python,5e中讲到.Python的函数参数传递机制是对象引用. Arguments are passed by assignment (object reference). I ...

  9. 【转】Python函数默认参数陷阱

    [转]Python函数默认参数陷阱 阅读目录 可变对象与不可变对象 函数默认参数陷阱 默认参数原理 避免 修饰器方法 扩展 参考 请看如下一段程序: def extend_list(v, li=[]) ...

  10. Python面试题目之Python函数默认参数陷阱

    请看如下一段程序: def extend_list(v, li=[]): li.append(v) return li list1 = extend_list(10) list2 = extend_l ...

随机推荐

  1. PageOffice 6 版本最简单的打开保存文件

    在OA办公.文档流转等各个Web系统中,实现最简单的打开编辑保存文件功能,调用PageOffice只需要几行代码就可以完成. 后端代码 在后端编写代码调用webOpen方法打开文件之前给SaveFil ...

  2. diffusion model(一):DDPM技术小结 (denoising diffusion probabilistic)

    发布日期:2023/05/18 主页地址:http://myhz0606.com/article/ddpm 1 从直觉上理解DDPM 在详细推到公式之前,我们先从直觉上理解一下什么是扩散 对于常规的生 ...

  3. 多进程池Flask实战应用

    多进程池Flask实战应用 import json import math import flask from concurrent.futures import ProcessPoolExecuto ...

  4. gorm指定数据字段名字

    type Products struct { gorm.Model SaleNum uint ` json:"saleNum"` CarNum uint ` json:" ...

  5. MyBatis两级缓存机制详解

    缓存是提高软硬件系统性能的一种重要手段:硬件层面,现代先进CPU有三级缓存,而MyBatis也提供了缓存机制,通过缓存机制可以大大提高我们查询性能. 一级缓存 ​ Mybatis对缓存提供支持,但是在 ...

  6. SpringAi

    Spring AI 初学 Spring AI 官方地址 "spring 不生产 AI,只是 AI 工具的搬运工" 项目可以查看gitee Open AI 前期准备 Open AI官 ...

  7. 不到200行用Vue实现类似Swiper.js的轮播组件

    前言 大家在开发过程中,或多或少都会用到轮播图之类的组件,PC和Mobile上使用 Swiper.js ,小程序上使用swiper组件等. 本文将详细讲解如何用Vue一步步实现的类似Swiper.js ...

  8. Qt-FFmpeg开发-视频播放【硬解码】(2)

    Qt-FFmpeg开发-视频播放[硬解码] 目录 Qt-FFmpeg开发-视频播放[硬解码] 1.概述 2.实现效果 3.FFmpeg硬解码流程 4.主要代码 6.完整源代码 更多精彩内容 个人内容分 ...

  9. linux开机出现initramfs无法进入系统

    linux开机出现initramfs无法进入系统 开机后进入initramfs模式,无法进入系统时不要慌: 想一想自己根分区的文件系统名是什么,有的人的是/dev/sda1,有的人的是/dev/sda ...

  10. 规则引擎easyRules中组合规则的使用

    这两天想要做支付路由,要根据支付信息将支付请求转发到不同的支付渠道,可能要用到规则引擎,于是研究了一下开源的规则引擎产品.规则引擎核心知识与开源产品对比选型 ,这篇文章对规格引擎和开源产品有详细的介绍 ...