Go 语言中的 Slice 陷阱:如何避免常见错误

news2024/11/28 6:40:40

Go 语言中的 Slice 陷阱:如何避免常见错误

  • 前言
    • slice 作为函数 / 方法的参数进行传递的陷阱
    • slice 通过 make 函数初始化,后续操作不当所造成的陷阱
    • 性能陷阱
  • 小结

耐心和持久胜过激烈和狂热。

哈喽大家好,我是陈明勇,本文介绍的内容是 Go 语言中的 Slice 的常见陷阱以及如何避免这些错误。如果本文对你有帮助,不妨点个赞,如果你是 Go 语言初学者,不妨点个关注,一起成长一起进步,如果本文有错误的地方,欢迎指出!

前言

Go 语言提供了很多方便的数据类型,其中包括 slice。然而,由于 slice 的特殊性质,在使用过程中易犯一些错误,如果不注意,可能导致程序出现意外行为。本文将详细介绍 使用 slice 时易犯的一些错误,帮助读者更好的使用 Goslice,避免犯错误。

slice 作为函数 / 方法的参数进行传递的陷阱

slice 作为参数进行传递,有一些地方需要注意,先说结论:

  • 1、在函数里修改切片元素的值,原切片的值也会被改变

    • 若想修改新切片的值,而不影响原切片的值,可以对原切片进行深拷贝:
      • 通过 copy(dst, src []Type) int 函数将原切片的元素拷贝到新切片中:此函数在拷贝时,会基于两个切片中,最小长度为基础去拷贝,也就是初始化新切片时,长度必须大于等于原切片的长度
  • 2、在函数里通过 append 方法,对切片执行追加元素的操作,可能会引起切片扩容,导致内存分配的问题,可能会对程序的性能 造成影响

    • 为避免切片扩容,导致内存分配,对程序的性能造成影响,在初始化切片时,应该根据使用场景,指定一个合理 cap 参数。
  • 3、在函数里通过 append 函数,对切片执行追加元素的操作,原切片里不存在新元素

    • 若想实现执行 append 函数之后,原切片也能得到新元素;需将函数的参数类型由 切片类型 改成 切片指针类型

通过例子来感受一下上面结论的由来:

package main

import "fmt"

func main() {
   s := []int{0, 2, 3}
   fmt.Printf("切片的长度:%d, 切片的容量:%d, 切片的元素:%v\n", len(s), cap(s), s) // 3 3 [0, 2, 3]
   sliceOperation(s)
   fmt.Printf("切片的长度:%d, 切片的容量:%d, 切片的元素:%v\n", len(s), cap(s), s) // 3 3 [1, 2, 3]
}

func sliceOperation(s []int) {
   s[0] = 1
   s = append(s, 4)
   fmt.Printf("切片的长度:%d, 切片的容量:%d, 切片的元素:%v\n", len(s), cap(s), s) // 4 6 [1, 2, 3]
}
  • 首先定义并初始化切片 s,切片里有三个元素;
  • 调用 sliceOperation 函数,将切片作为参数进行传递;
  • 在函数里修改切片的第一个元素的值为 1,然后通过 append 函数插入元素 4,此时函数里的切片 由于容量不够,s 的容量被扩大了,变成 原 cap * 2 = 3 * 2 = 6
  • 打印结果已注释在代码里,通过打印结果可知:
    • 在函数里修改切片的第一个元素的值,原切片元素的值也会改变;
    • 在函数里通过 append 函数,向切片追加元素 4,原切片并没有此元素;
    • 函数里的切片扩容了,原切片却没有。

由于切片是引用类型,因此在函数修改切片元素的值,原切片的元素值也会改变。

有的人可能会产生以下两个疑问

  • 1、既然切片是引用类型,为什么通过 append 追加元素,原切片 s 却没有新元素?
  • 2、为什么函数里的切片扩容了,原切片却没有?

在探究这两个问题之前,我们需要了解切片的数据结构:

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

切片包含三个字段:array (指针类型,指向一个数组)、len (切片的长度)、cap (切片的容量)。

知道了切片的数据结构,我们通过图片来直观地看看切片 s

在这里插入图片描述

切片 s 没有被修改之前,在内存中是以上图所描述的形式存在,array 指针变量指向数组 [0, 2, 3],长度为 3,容量为 3

在这里插入图片描述

在执行 sliceOperation 函数之后,原切片 ssliceOperation 函数里的切片 s 如上图所示。


通过上上图和上图对比可知,底层数组 [0, 2, 3] 的第一个元素的值被修改为 1,然后追加元素 4,此时函数里的切片发生变化,长度 3 → 4,容量 3 → 6 变成原来的两倍,底层数组的长度也由 3 → 6

由于原切片 s 的长度为 3array 指针所指向的区域只有 [1, 2, 3],这也是为什么在函数里新增了 元素 4,在原切片 s 里看不到的原因。


第一个问题解决了,我们来思考第二个问题的原因:

Go 中,函数 / 方法的参数传递方式为值传递main 函数将 s 传递过来,sliceOperation 函数用 s 去接收,此时的 s 为新的切片,只不过它们所指向的底层数组为同一个,长度和容量也是一样。而扩容操作是在新切片上进行的,因此原切片不受影响。

slice 通过 make 函数初始化,后续操作不当所造成的陷阱

使用 make 函数初始化切片后,如果在后续操作中没有正确处理切片长度,容易造成以下陷阱:

  • 越界访问:如果访问超出切片实际长度的索引,则会导致 index out of range 错误,例如:
    func main() {
       s := make([]int, 0, 4)
       s[0] = 1 // panic: runtime error: index out of range [0] with length 0
    }
    
    通过 make([]int, 0, 4) 初始化切片,虽说容量为 4,但是长度为 0,如果通过索引去赋值,会发生panic;为避免 panic,可以通过 s := make([]int, 4)s := make([]int, 4, 4) 对切片进行初始化。
  • 切片初始化不当,通过 append 函数追加新元素的位置可能于预料之外
    func main() {
       s := make([]int, 4)
       s = append(s, 1)
       fmt.Println(s[0]) // 0
    
       s2 := make([]int, 0, 4)
       s2 = append(s2, 1)
       fmt.Println(s2[0]) // 1
    }
    
    通过打印结果可知,对于切片 s,元素 1 没有被放置在第一个位置,而对于切片 s2,元素 1 被放置在切片的第一个位置。这是因为通过 make([]int, 4)make([]int, 0, 4) 初始化切片,底层所指向的数组的值是不一样的:
    • 第一种初始化的方式,切片的长度和容量都为 4,底层所指向的数组长度也是 4,数组的值为 [0, 0, 0, 0],每个位置的元素被赋值为零值s = append(s, 1) 执行后,s 切片的值为 [0, 0, 0, 0, 1]
    • 第二种初始化的方式,切片的长度为 0,容量为 4,底层所指向的数组长度为 0,数组的值为 []s2 = append(s2, 1) 执行后,s2 切片的值为 [1]
    • 通过 append 向切片追加元素,会执行尾插操作。如果我们需要初始化一个空切片,然后从第一个位置开始插入元素,需要避免 make([]int, 4) 这种初始化的方式,否则添加的结果会在预料之外。

性能陷阱

  • 内存泄露

    内存泄露是指程序分配内存后不再使用该内存,但未将其释放,导致内存资源被浪费。

    切片引用切片场景:如果一个切片有大量的元素,而它只有少部分元素被引用,其他元素存在于内存中,但是没有被使用,则会造成内存泄露。代码示例如下:

      var s []int
    
      func main() {
         sliceOperation()
         fmt.Println(s)
      }
    
      func sliceOperation() {
         a := make([]int, 0, 10)
         a = append(a, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
         s = a[0:4]
      }
    

    上述代码中,切片 a 的元素有 10 个,而切片 s 是基于 a 创建的,它底层所指向的数组与 a 所指向的数组是同一个,只不过范围为前四个元素,而后六个元素依然存在于内存中,却没有被使用,这样会造成内存泄露。为了避免内存泄露,我们可以对代码进行改造:
    s = a[0:4]s = append(s, a[0:4]...),通过 append 进行元素追加,这样切片 a 底层的数组没有被引用,后面会被 gc

  • 扩容

    扩容陷阱在前面的例子也提到过,通过 append 方法,对切片执行追加元素的操作,可能会引起切片扩容,导致内存分配的问题。

      func main() {
         s := make([]int, 0, 4)
         fmt.Printf("切片的长度:%d, 切片的容量:%d\n", len(s), cap(s)) // 4 4
         s = append(s, 1, 2, 3, 4, 5)
         fmt.Printf("切片的长度:%d, 切片的容量:%d\n", len(s), cap(s)) // 5 8
      }
    

    切片扩容,可能会对程序的性能 造成影响;为避免此情况的发生,应该根据使用场景,估算切片的容量,指定一个合理 cap 参数。

小结

本篇文章主要介绍了使用 Go slice 的过程中可能会遇到的陷阱以及如何避免这些错误,帮助读者更好地使用 Go slice

如果读者有遇到别的陷阱,欢迎在评论区留言探讨。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/345182.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Lesson 7.1 无监督学习算法与 K-Means 快速聚类

文章目录一、聚类算法与无监督学习二、K-Means 快速聚类的算法原理1. K-Means 快速聚类的基本执行流程2. K-Means 快速聚类的背后的数学意义三、K-Means 快速聚类的 sklearn 实现方法1. sklearn 中实现 K-Means 快速快速聚类2. 轮廓系数基本概念与 sklearn 中实现方法从现在开始…

【基于jeeSite框架】SpringBoot+poi+Layui自定义列表导出

文章目录功能效果思路代码前台后台easyPoi,easyExcel,poi三者的区别poipoi依赖导出ExcelHSSF方式导出XSSF方式导出SXSSF方式导出导入excelHSSF方式导入XSSF方式导入SXSSF方式导入easyPoi依赖包采用注解导出导入easyExcel依赖采用注解导出导入API文档easyPoi操作文档…

内网渗透(二十五)之Windows协议认证和密码抓取-使用Hashcat和在线工具破解NTLM Hash

系列文章第一章节之基础知识篇 内网渗透(一)之基础知识-内网渗透介绍和概述 内网渗透(二)之基础知识-工作组介绍 内网渗透(三)之基础知识-域环境的介绍和优点 内网渗透(四)之基础知识-搭建域环境 内网渗透(五)之基础知识-Active Directory活动目录介绍和使用 内网渗透(六)之基…

说说Knife4j

Knife4j是一款基于Swagger2的在线API文档框架使用Knife4j, 需要 添加Knife4j的依赖当前建议使用的Knife4j版本, 只适用于Spring Boot2.6以下版本, 不含Spring Boot2.6 在主配置文件(application.yml)中开启Knife4j的增强模式必须在主配置文件中进行配置, 不要配置在个性化配置文…

Java 代理模式详解

1、代理模式 代理模式是一种比较好理解的设计模式。简单来说就是 我们使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。 代理模式的主要作用是扩展目标对象…

C++——运算符重载

1、运算符重载的概念 运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。运算符重载的目的是让语法更加简洁运算符重载不能改变本来寓意,不能改变基础类型寓意运算符重载的本质是另一种函数调用…

【半监督医学图像分割 2021 TMI】SimCVD 论文翻译

文章目录【半监督医学图像分割 2021 TMI】SimCVD 论文翻译摘要1. 介绍2. 相关工作3. 方法3.1 总览3.2 任务制定3.3 基础体系结构3.4 边缘对比蒸馏4. 实验4.1 数据集和预处理4.2 实现细节5. 结论5.1 实验:LA5.2 实验:Pancreas6. 消融研究6.1 边界感知对比…

嵌入式ARM工业边缘计算机BL302的CAN总线接口如何设置?

CAN 接口如图所示,输入如下命令: ifconfig -a //查看所有网卡 如果 FlexCAN 驱动工作正常的话就会看到 CAN 对应的网卡接口,如图。从图中可 以看出,有一个名为“can0”的网卡,这个就是 BL302 板上的 CAN1 接口对应的 c…

借助亚马逊云科技,Early Data完成数据云上安家、实现降本增效

数字经济时代,伴随着大数据应用的不断深入,企业对用户及市场发展动向的判断正变得愈加精准。数据资产不再是虚无缥缈的东西,而是可以帮助企业切切实实找到业务增长点,洞悉潜在商机,拥有巨大潜力的“宝藏”。IDC数据显示…

【 mybatis的工作流程】

目录一.mybatis执行流程二.使用工具类简化项目(反射的体现)2.1 Sqlsession工厂对像2.2 工具类(可直接放在工具类使用)一.mybatis执行流程 1.1 读取主配置文件mybatis-config.xml,获得运行环境和数据库连接。 1.2 加载映…

对象之间的关系

目录1. 依赖2. 关联3. 聚合4. 组合Java的对象/类之间有四种关系:依赖、关联、组合、聚合。 1. 依赖 依赖(Dependency): 一个对象的功能依赖于另一个对象。 类比:人类生存依赖食物和空气 体现:被依赖者体…

Ethercat系列(5)TWcat3激活过程的协议分析(续1)

顺序写系统时间偏移从-》主顺序写时间延迟主-》从从-》主顺序写分布式时钟启动主-》从从-》主读多重写系统时间主-》从从-》主顺序写应用层控制主-》从从-》主顺序读错误计数器主-》从从-》主顺序读应用层状态主-》从从-》主顺序读应用层,广播写错误计数器主-》从从…

2023 年首轮土地销售活动来了 与 The Sandbox 一起体验「体素狂热」!

2 月 14 日晚上 11 点,开始你的体素冒险。 The Sandbox 很高兴推出 2023 年的第一次土地销售活动。欢迎来到「体素狂热 (Voxel Madness)」! 简要概括 土地销售抽奖活动将于北京时间 2 月 14 日星期二晚上 11 点开始 「体素狂热」 土地销售活动将于 2 月…

企业公众号菜单添加视频的完整教程(组图)

公众号实用教程,公众号运营教程。 有公众号的朋友,看过来,今天来教教大家,在企业微信公众号中如何调用视频列表。先上一个效果吧: 最终效果的截图 -关注酷播公众号,查看实际的应用效果(在本公众…

【shell 编程大全】脚本交互 以及表达式

脚本交互 以及表达式 1. 概述 嗨,我又来继续分享了。今天分享的内容是脚本交互,再开始接下来的分享开始之前,让我们来回顾下上一次(【shell 编程大全】内容格式化以及多样化输出)的内容: 内容格式化 重定向管道符后台执行信息符号…

C语言学习_DAY_3_基本数据类型_运算符与表达式【C语言学习笔记】

目录 I. 基本数据类型 II. 复杂的输出和输入语句编写 III. 运算符与表达式 III.I 算术运算符 III.II 关系运算符 III.III 逻辑运算符 III.IV 位运算符 III.V 三目运算符 III.VI 逗号运算符 高质量博主,点个关注不迷路🌸🌸&#x1f3…

电脑怎么备份文件?含泪分享3种免费的方法

电脑里面保存着我们很多文件数据,比如工作文档、珍贵的照片、喜欢的音乐或者视频等。你很可能会遇到下面的原因而丢失数据:清空回收站,或者使用永久删除文件的方法;电脑出现故障,比如硬盘故障、系统崩溃;病…

Python实现基于openCV+百度智能云平台实现《1:N人脸考勤机》文章最后附带源码!

目录 一、 项目介绍 1.1 项目名称 1.2 项目简介 1.3 项目物料 1.4 技术栈 二、 项目架构 三、项目细节 3.1 环境搭建 3.2 利用opencv实现摄像头调取及相关图像的采集 3.3 利用aips上传图像和结果返回 3.4 结果优化和处理 3.5 可扩展性 3.6 遗留问题和…

中睿天下获评2022年度中关村网信联盟工业互联网标准工作特别贡献奖

近日,中关村网络安全与信息化产业联盟(简称“CCAIA”)于线上召开联盟第二届第五次会员大会,来自全国的联盟成员代表、行业专家等100余人出席会议,共同听取了联盟2022年工作总结与2023年工作计划,大会设置了…

春招Leetcode刷题日记-D2-贪心算法-训练专题

D2-贪心算法-训练专题贪心算法使用总结力扣605. 种花问题思路代码力扣763. 划分字母区间思路代码另一种思路贪心算法使用总结 当我们意识到,满足贪心算法三要素时候,解决贪心算法问题的时候,一定要遵循的步骤: 1、读懂题目限制条…