【golang学习之旅】复杂数据类型——切片(slice)

2024-08-25 23:04

本文主要是介绍【golang学习之旅】复杂数据类型——切片(slice),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

系列文章

【golang学习之旅】使用VScode安装配置Go开发环境
【golang学习之旅】报错:a declared but not used
【golang学习之旅】Go 的基本数据类型
【golang学习之旅】深入理解字符串string数据类型
【golang学习之旅】go mod tidy
【golang学习之旅】记录一次 panic case : reflect: reflect.Value.SetInt using unaddressable value
【golang学习之旅】记录一次 error case : full error output: cc1: error: unrecognized command line option
【golang学习之旅】Go程序快速开始 & Go程序开发的基本注意事项
【golang学习之旅】Go语言常用转义字符
【golang学习之旅】Go中的变量(1)
【golang学习之旅】Go中的变量——基本数据类型(1)
【golang学习之旅】Go中的变量——基本数据类型(2)
【golang学习之旅】复杂数据类型——指针 & 函数
【golang学习之旅】延迟调用——defer
【golang学习之旅】复杂数据类型——数组


  • 系列文章
  • 1. 理解切片(slice)
  • 2. 切片的定义
    • 2.1 切片的内存结构
    • 2.2 创建切片
  • 3. 操作切片
    • 3.1 添加切片元素
      • 3.1.1 append
      • 3.1.2 copy
    • 3.2 删除切片元素


1. 理解切片(slice)

切片(slice)是对数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型)。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集

简单地说,切片就是一种简化版的动态数组

因为动态数组的长度是不固定,切片的长度自然也就不能是类型的组成部分了。数组虽然有适用它们的地方,但是数组的类型和操作都不够灵活,因此在 Go 代码中数组使用的并不多。而切片则使用得相当广泛,理解切片的原理和用法是一个 Go 程序员的必备技能。


2. 切片的定义

2.1 切片的内存结构

我们先看看切片的结构定义,reflect.SliceHeader

切片在内存中的组织方式实际上是一个有 3 个域的结构体:指向相关数组的指针,切片长度以及切片容量。

type SliceHeader struct {  Data uintptr // 指向底层数组的指针  Len  int     // 切片的长度(即元素的数量)  Cap  int     // 切片的容量(即底层数组可以容纳的元素数量)  
}

现在,考虑以下两个切片的声明和初始化:

x := []int{2, 3, 5, 7, 11}  
y := x[1:3]
  • x切片的内存结构
    • Data:指向一个包含至少5个int类型元素的数组的指针。具体地,这个数组包含元素[2, 3, 5, 7, 11]。
    • Len:5,因为x包含5个元素。
    • Cap:5,因为底层数组也恰好有5个元素的空间,所以切片x的容量等于其长度。
  • y切片的内存结构
    • Data:同样指向那个包含[2, 3, 5, 7, 11]的数组,但切片y的视角是从第二个元素开始的,即3(索引为1,因为索引从0开始)。
    • Len:2,因为y是通过x[1:3]创建的,所以它包含x中从索引1到索引2(不包含3)的元素,即[3, 5]。
      Cap:4,尽管y只包含了2个元素,但它的容量是从y的起始元素到原数组x末尾的元素数量,即[3, 5, 7, 11]这四个元素的空间。这是因为y和x共享同一个底层数组,所以y的容量受到这个共享数组的限制。
  • 示意图
    下面是一个简化的示意图,用于说明这两个切片的内存结构:
    在这里插入图片描述

2.2 创建切片

在Go中,我们可以通过多种方式创建切片:

  1. 直接使用数组创建切片:通过数组的一部分来初始化切片。
a := [5]int{1, 2, 3, 4, 5}  
s := a[1:4] // s是一个切片,包含a中索引为1到3的元素(即2, 3, 4)
  1. 使用make函数:make函数是Go中用于分配和初始化内置类型的内置函数,也可以用来创建切片。
s := make([]int, 0, 5) // 创建一个长度为0、容量为5的int类型切片
  1. 切片字面量:直接使用切片字面量创建切片。
s := []int{1, 2, 3} // 直接初始化一个切片

还有一些比较特殊的切片:

  1. nil 切片。未初始化的切片默认值为nil
var a []int 
  1. 空切片,也叫零值切片。和 nil 不相等, 一般用来表示一个空的集合。len 和 cap 都为 0。空切片在内部拥有一个非nil的、零长度的底层数组
var b = []int{} 
  • 在判断一个切片是否为空时,一般通过 len 获取切片的长度来判断,一般很少将切片和 nil 值做直接的比较

3. 操作切片

3.1 添加切片元素

3.1.1 append

使用append函数可以向切片末尾追加一个或多个元素。如果追加的元素超出了当前切片的容量,append会分配一个新的底层数组,并将原切片的内容以及新元素复制到新数组中。

var a []int
a = append(a, 1)               // 追加 1 个元素
a = append(a, 1, 2, 3)         // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加 1 个切片, 切片需要解包

这里需要注意...(三个点,称为“变参”或“展开运算符”)。在Go中,当你将切片或数组作为函数参数,并且希望将它的所有元素作为独立的参数传递给函数时,可以使用这个操作符。在这个上下文中,...[]int{1,2,3} 切片中的所有元素“展开”成独立的参数,传递给 append 函数。

除了在切片的尾部追加,我们还可以在切片的开头添加元素:

var a = []int{1,2,3}
a = append([]int{0}, a...)        // 在开头添加 1 个元素
a = append([]int{-3,-2,-1}, a...) // 在开头添加 1 个切片

在开头一般都会导致内存的重新分配,而且会导致已有的元素全部复制 1 次。因此,从切片的开头添加元素的性能一般要比从尾部追加元素的性能差很多。

3.1.2 copy

copy函数可以用来复制切片的内容:

src := []int{1, 2, 3}  
dst := make([]int, 3)  
copy(dst, src) // 现在dst是[1, 2, 3]的拷贝

可以用 copy 和 append 组合可以避免创建中间的临时切片,同样是完成添加元素的操作:

a = append(a, 0)     // 切片扩展 1 个空间
copy(a[i+1:], a[i:]) // a[i:] 向后移动 1 个位置
a[i] = x             // 设置新添加的元素
  • 第一句 append 用于扩展切片的长度,为要插入的元素留出空间
  • 第二句 copy 操作将要插入位置开始之后的元素向后挪动一个位置
  • 第三句真实地将新添加的元素赋值到对应的位置

用 copy 和 append 组合也可以实现在中间位置插入多个元素(也就是插入一个切片):

用 copy 和 append 组合也可以实现在中间位置插入多个元素(也就是插入一个切片):

a = append(a, x...)       // 为 x 切片扩展足够的空间
copy(a[i+len(x):], a[i:]) // a[i:] 向后移动 len(x) 个位置
copy(a[i:], x)            // 复制新添加的切片

稍显不足的是,在第一句扩展切片容量的时候,扩展空间部分的元素复制是没有必要的。没有专门的内置函数用于扩展切片的容量,append 本质是用于追加元素而不是扩展容量,扩展切片容量只是 append 的一个副作用。

3.2 删除切片元素

根据要删除元素的位置有三种情况:从开头位置删除,从中间位置删除,从尾部删除。

其中删除切片尾部的元素最快:

a = []int{1, 2, 3}
a = a[:len(a)-1]   // 删除尾部 1 个元素
a = a[:len(a)-N]   // 删除尾部 N 个元素

删除开头的元素可以直接移动数据指针:

a = []int{1, 2, 3}
a = a[1:] // 删除开头 1 个元素
a = a[N:] // 删除开头 N 个元素

删除开头的元素也可以不移动数据指针,但是将后面的数据向开头移动。可以用 append 原地完成(所谓原地完成是指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化):

a = []int{1, 2, 3}
a = append(a[:0], a[1:]...) // 删除开头 1 个元素
a = append(a[:0], a[N:]...) // 删除开头 N 个元素

这篇关于【golang学习之旅】复杂数据类型——切片(slice)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1106903

相关文章

Go学习记录之runtime包深入解析

《Go学习记录之runtime包深入解析》Go语言runtime包管理运行时环境,涵盖goroutine调度、内存分配、垃圾回收、类型信息等核心功能,:本文主要介绍Go学习记录之runtime包的... 目录前言:一、runtime包内容学习1、作用:① Goroutine和并发控制:② 垃圾回收:③ 栈和

MySQL复杂SQL之多表联查/子查询详细介绍(最新整理)

《MySQL复杂SQL之多表联查/子查询详细介绍(最新整理)》掌握多表联查(INNERJOIN,LEFTJOIN,RIGHTJOIN,FULLJOIN)和子查询(标量、列、行、表子查询、相关/非相关、... 目录第一部分:多表联查 (JOIN Operations)1. 连接的类型 (JOIN Types)

Golang 日志处理和正则处理的操作方法

《Golang日志处理和正则处理的操作方法》:本文主要介绍Golang日志处理和正则处理的操作方法,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考... 目录1、logx日志处理1.1、logx简介1.2、日志初始化与配置1.3、常用方法1.4、配合defer

Android学习总结之Java和kotlin区别超详细分析

《Android学习总结之Java和kotlin区别超详细分析》Java和Kotlin都是用于Android开发的编程语言,它们各自具有独特的特点和优势,:本文主要介绍Android学习总结之Ja... 目录一、空安全机制真题 1:Kotlin 如何解决 Java 的 NullPointerExceptio

golang float和科学计数法转字符串的实现方式

《golangfloat和科学计数法转字符串的实现方式》:本文主要介绍golangfloat和科学计数法转字符串的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望... 目录golang float和科学计数法转字符串需要对float转字符串做处理总结golang float

golang实现延迟队列(delay queue)的两种实现

《golang实现延迟队列(delayqueue)的两种实现》本文主要介绍了golang实现延迟队列(delayqueue)的两种实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的... 目录1 延迟队列:邮件提醒、订单自动取消2 实现2.1 simplChina编程e简单版:go自带的time

Golang实现Redis分布式锁(Lua脚本+可重入+自动续期)

《Golang实现Redis分布式锁(Lua脚本+可重入+自动续期)》本文主要介绍了Golang分布式锁实现,采用Redis+Lua脚本确保原子性,持可重入和自动续期,用于防止超卖及重复下单,具有一定... 目录1 概念应用场景分布式锁必备特性2 思路分析宕机与过期防止误删keyLua保证原子性可重入锁自动

golang 对象池sync.Pool的实现

《golang对象池sync.Pool的实现》:本文主要介绍golang对象池sync.Pool的实现,用于缓存和复用临时对象,以减少内存分配和垃圾回收的压力,下面就来介绍一下,感兴趣的可以了解... 目录sync.Pool的用法原理sync.Pool 的使用示例sync.Pool 的使用场景注意sync.

golang中slice扩容的具体实现

《golang中slice扩容的具体实现》Go语言中的切片扩容机制是Go运行时的一个关键部分,它确保切片在动态增加元素时能够高效地管理内存,本文主要介绍了golang中slice扩容的具体实现,感兴趣... 目录1. 切片扩容的触发append 函数的实现2. runtime.growslice 函数gro

golang实现动态路由的项目实践

《golang实现动态路由的项目实践》本文主要介绍了golang实现动态路由项目实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习... 目录一、动态路由1.结构体(数据库的定义)2.预加载preload3.添加关联的方法一、动态路由1