【Golang】切片(slice)

切片

切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型,这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集,需要注意的是,终止索引标识的项不包括在切片内。

Go语言中切片的内部结构包含地址、大小和容量,切片一般用于快速地操作一块数据集合,如果将数据集合比作切糕的话,切片就是你要的“那一块”,切的过程包含从哪里开始(切片的起始位置)及切多大(切片的大小),容量可以理解为装切片的口袋大小,如下图所示:

在这里插入图片描述

切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。

从连续内存区域生成切片是常见的操作,格式如下:

slice [开始位置 : 结束位置]

语法说明如下:

  • slice:表示目标切片对象;
  • 开始位置:对应目标切片对象的索引;
  • 结束位置:对应目标切片的结束索引。

从数组生成切片,代码如下:

import "fmt"

func main() {
    
    
   var a = [3]int{
    
    1, 2, 3}

   fmt.Println(a, a[1:2])
}

从数组或切片生成新的切片拥有如下特性:

  • 取出的元素数量为:结束位置 - 开始位置;
  • 取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[len(slice)-1] 获取;a[len(a)-1]
  • 当缺省开始位置时,表示从连续区域开头到结束位置;
  • 当缺省结束位置时,表示从开始位置到整个连续区域末尾;
  • 两者同时缺省时,与切片本身等效;
  • 两者同时为 0 时,等效于空切片,一般用于切片复位

直接声明新的切片

除了可以从原有的数组或者切片中生成切片外,也可以声明一个新的切片,每一种类型都可以拥有其切片类型,表示多个相同类型元素的连续集合,因此切片类型也可以被声明,切片类型声明格式如下:

var name []Type
  • name 表示切片的变量名
  • Type 表示切片对应的元素类型
package main

import "fmt"

func main() {
    
    
   // 声明字符串切片
   var strList []string

   // 声明整型切片
   var numList []int

   // 声明一个空切片  分配了内存 但是没有元素
   var numListEmpty = []int{
    
    }

   // 输出3个切片
   fmt.Println(strList, numList, numListEmpty)

   // 输出3个切片大小
   fmt.Println(len(strList), len(numList), len(numListEmpty))

   // 切片判定空的结果
   fmt.Println(strList == nil)
   fmt.Println(numList == nil)
   fmt.Println(numListEmpty == nil)
}

切片是动态结构 只能与nil判断相等,不能互相判等。

如果需要动态地创建一个切片,可以使用 make() 内建函数,格式如下:

make( []Type, size, cap )
  • Type 是指切片的元素类型
  • size 指的是为这个类型分配多少个元素
  • cap 为预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题

代码:

package main

import "fmt"

func main() {
    
    
   a := make([]int, 2)
   b := make([]int, 2, 10)

   fmt.Println(a, b)
   fmt.Println(len(a), len(b))
}

结果:

[0 0] [0 0]
2 2

a和b均是预分配了2个元素的切片,只是b的内部存储空间已经分配了10个,但实际上只是用了2个元素。
容量不会影响当前的元素个数,因此a和b取len都是2

使用make()函数一定发生了内存分配的操作。但给定开始与结束位置的切片只是将新的切片结构指向已经分配好的内存区域,不会发生内存分配操作。

append()函数为切片添加元素

Go语言的内建函数append()可以为切片动态添加元素,每个切片会指向一片内存空间,这片空间能容纳一定数量的元素。当空间不能容纳足够多的元素时,切片就会进行扩容(2倍),扩容操作往往发生在append()函数调用时。

代码:

package main

import "fmt"

func main() {
    
    
   //声明一个切片
   var numbers []int
   
   for i := 0; i < 10; i++ {
    
    
      numbers = append(numbers, i) //给切片中添加元素
      fmt.Printf("len: %d  cap: %d pointer: %p\n", len(numbers), cap(numbers), numbers)
   }
}

结果:

len: 1  cap: 1 pointer: 0xc000018080
len: 2  cap: 2 pointer: 0xc0000180a0
len: 3  cap: 4 pointer: 0xc00001a040
len: 4  cap: 4 pointer: 0xc00001a040
len: 5  cap: 8 pointer: 0xc00001e0c0
len: 6  cap: 8 pointer: 0xc00001e0c0
len: 7  cap: 8 pointer: 0xc00001e0c0
len: 8  cap: 8 pointer: 0xc00001e0c0
len: 9  cap: 16 pointer: 0xc000102000
len: 10  cap: 16 pointer: 0xc000102000

可以看到容量是以2倍进行扩增的。在进行扩增之后,地址也可能发生改变

也可以一次性添加多个元素

package main

import "fmt"

func main() {
    
    
   //声明一个切片
   var car []string

   //添加一个元素
   car = append(car, "oldDriver")
   //添加多个元素
   car = append(car, "ice", "sniper", "monk")

   fmt.Println(car)
   //添加切片
   team := []string{
    
    "pig", "cat"}

   car = append(car, team...)

   fmt.Println(car)

}

结果:

[oldDriver ice sniper monk]
[oldDriver ice sniper monk pig cat]

team后添加的...表示将team整个添加到car的后面

复制切到另一个切片

使用Go语言内建的copy函数,可以迅速地将一个切片的数据复制到另一个切片空间中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。

copy() 函数的使用格式如下:

copy( destSlice, srcSlice []T) int
  • srcSlice 为数据来源切片
  • destSlice 为复制的目标(也就是将 srcSlice 复制到 destSlice),目标切片必须分配过空间且足够承载复制的元素个数,并且来源和目标的类型必须一致,copy() 函数的返回值表示实际发生复制的元素个数。

代码:

package main

import "fmt"

func main() {
    
    

   // 设置元素数量为1000
   const elementCount = 1000

   // 预分配足够多的元素切片
   srcData := make([]int, elementCount)

   // 将切片赋值
   for i := 0; i < elementCount; i++ {
    
    
      srcData[i] = i
   }

   // 引用切片数据
   refData := srcData

   // 预分配足够多的元素切片
   copyData := make([]int, elementCount)
   // 将数据复制到新的切片空间中
   copy(copyData, srcData)

   // 修改原始数据的第一个元素
   srcData[0] = 999

   // 打印引用切片的第一个元素
   fmt.Println(refData[0])

   // 打印复制切片的第一个和最后一个元素
   fmt.Println(copyData[0], copyData[elementCount-1])

   // 复制原始数据从4到6(不包含)
   copy(copyData, srcData[4:6])

   for i := 0; i < 5; i++ {
    
    
      fmt.Printf("%d ", copyData[i])
   }
}

结果:

999
0 999
4 5 2 3 4
  • 第 8 行,定义元素总量为 1000。
  • 第 11 行,预分配拥有 1000 个元素的整型切片,这个切片将作为原始数据。
  • 第 14~16 行,将 srcData 填充 0~999 的整型值。
  • 第 19 行,将 refData 引用 srcData,切片不会因为等号操作进行元素的复制。
  • 第 22 行,预分配与 srcData 等大(大小相等)、同类型的切片 copyData。
  • 第 24 行,使用 copy() 函数将原始数据复制到 copyData 切片空间中。
  • 第 27 行,修改原始数据的第一个元素为 999。
  • 第 30 行,引用数据的第一个元素将会发生变化。
  • 第 33 行,打印复制数据的首位数据,由于数据是复制的,因此不会发生变化。
  • 第 36 行,将 srcData 的局部数据复制到 copyData 中。
  • 第 38~40 行,打印复制局部数据后的 copyData 元素。

从切片中删除元素

Go语言并没有对删除切片元素提供专用的语法或者接口,需要使用切片本身的特性来删除元素,根据要删除元素的位置有三种情况,分别是从开头位置删除、从中间位置删除和从尾部删除,其中删除切片尾部的元素速度最快。

从开头位置删除

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

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个元素

还可以用 copy() 函数来删除开头的元素:

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

从中间位置删除

对于删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用 append 或 copy 原地完成:

a = []int{
    
    1, 2, 3, ...}
a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素
a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素
a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素

从尾部删除

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

删除开头的元素和删除尾部的元素都可以认为是删除中间元素操作的特殊情况,下面来看一个示例。

【示例】删除切片指定位置的元素。

package main

import "fmt"

func main() {
    
    
   seq := []string{
    
    "a", "b", "c", "d", "e"}

   // 指定删除位置
   index := 2

   // 查看删除位置之前的元素和之后的元素
   fmt.Println(seq[:index], seq[index+1:])

   // 将删除点前后的元素连接起来
   seq = append(seq[:index], seq[index+1:]...)

   fmt.Println(seq)
}

代码输出结果:

[a b] [d e]
[a b d e]

代码说明如下:

  • 第 1 行,声明一个整型切片,保存含有从 a 到 e 的字符串。
  • 第 4 行,为了演示和讲解方便,使用 index 变量保存需要删除的元素位置。
  • 第 7 行,seq[:index] 表示的就是被删除元素的前半部分,值为 [1 2],seq[index+1:] 表示的是被删除元素的后半部分,值为 [4 5]。
  • 第 10 行,使用 append() 函数将两个切片连接起来。
  • 第 12 行,输出连接好的新切片,此时,索引为 2 的元素已经被删除。

Go语言中删除切片元素的本质是,以被删除元素为分界点,将前后两个部分的内存重新连接起来。

连续容器的元素删除无论在任何语言中,都要将删除点前后的元素移动到新的位置,随着元素的增加,这个过程将会变得极为耗时,因此,当业务需要大量、频繁地从一个切片中删除元素时,如果对性能要求较高的话,就需要考虑更换其他的容器了(如双链表等能快速从删除点删除元素)

使用range关键字来迭代切片

代码:

package main

import "fmt"

func main() {
    
    
   //创建一个切片
   slice := []int{
    
    10, 20, 30}
   //迭代每个元素,并显示值和地址和切片中的地址
   for index, value := range slice {
    
    
      fmt.Printf("%d  value: %d value-addr: %X,slice-addr %X\n", index, value, &amp;value, &amp;slice[index])
   }
}

结果:

0  value: 10 value-addr: C000018080,slice-addr C00001C078
1  value: 20 value-addr: C000018080,slice-addr C00001C080
2  value: 30 value-addr: C000018080,slice-addr C00001C088

因为迭代返回的变量是一个在迭代过程中根据切片依次赋值的新变量,所以 value 的地址总是相同的,要想获取每个元素的地址,需要使用切片变量和索引值(例如上面代码中的 &slice[index])

如果不需要索引,可以使用匿名变量名-来忽略这个值。

猜你喜欢

转载自blog.csdn.net/qq_45795744/article/details/125742113