如何更好的学习Golang中的切片数据类型

卡二条的技术圈

共 5133字,需浏览 11分钟

 ·

2021-11-29 19:17

含义

切片是一个种特殊的数组。是对数组的一个连续片段的引用,所以切片是一个引用类型。切片可以是数组中的一部分,也可以是由起始和终止索引标识的一些项的子集。切片有点像C语言里的指针,指针可以做运算,但代价是内存操作越界,切片在指针的基础上增加了大小,约束了切片对应的内存区域,切片使用中无法对切片内部的地址和大小进行手动调整,因此切片比指针更安全、强大。

定义

切片定义分为三中形式。依次从数组中生成、从切片中生成和全新定义一个切片。

三个要素

1.起始位置:切片引用数组的开始位置。

2.大小:切片中的元素个数。切片中的大小不能超过容量数量。可以使用len()函数对切片统计大小。

3.容量:切片最大可存的元素个数。如果空间不足以容纳足够多的元素,切片就会进行动态“扩容”,此时新切片的长度会发生改变。一般切片的扩容是按照扩容前容量的2倍。可以使用cap()函数对切片容量进行统计。

切片与数组的区别

  1. 切片是对数组中的连续引用。切片的初始位置指向数组的内存地址,如果切片的值改变,数组对应的值也会对应改变。

  2. 切片的长度是动态的,本质上是一个可变的动态数组。数组的长度在定义的时候就决定好了,后期是无法修改数组的长度的。

  3. 切片的长度是可以动态扩容的[如上面容量一次提到的]。

  4. 切片本身是不保存数据,它只是底层数组的表示。对切片所做的任何修改都将反应到底层数组中。

package main
import (
 "fmt"
)
func main() {
 numa := [3]int{787980}
 nums1 := numa[:]
 nums2 := numa[:]
 fmt.Println("array before change 1", numa)
 nums1[0] = 100
 fmt.Println("array after modification to slice nums1", numa)
 nums2[1] = 101
 fmt.Println("array after modification to slice nums2", numa)
}
// output
array before change 1 [78 79 80]
array after modification to slice nums1 [100 79 80]
array after modification to slice nums2 [100 101 80]

当多个切片共享一个底层数组时,每个切片的修改都将反映在底层数组中。

示例代码

// 通过数组定义切片
var array1 = [3]int{113}
fmt.Println("数组的元素分别是:", array1)
slice1 := array1[0:2]
slice1[1] = 11111
fmt.Println("数组的元素分别是:", array1)
// 输出结果
数组的元素分别是: [1 1 3]
数组的元素分别是: [1 11111 3]
``
## 切片定义分类

### 数组生成切片

#### 定义语法

```go
slice[起始位置:结束位置]

1.slice:表示切片的对象。例如从一个数组中生成切片则slice就是定义的数组名称。

2.起始位置:从数组中的某个元素的下标开始切,默认中0开始。

3.结束位置:切片的结束位置。也就是数组的某个元素下标位置。需要注意的是这里取的是开区间。如果需要取到数组的最后一个元素,结束位置这是数组的长度+1。

4.切片的长度:(切片的结束位置-切片的起始位置)。

示例代码

// 通过数组定义切片
array := []string{"A","B","C","D","E","F","G","H","I","G","K","L"}
slice := array(0:5)
// 打印结果为
[A B C D E]

// 使用make方式创建切片
slice1 := make([]string23)
slice1[0] = "1"
fmt.Println(slice1)

slice2 := make([]string23)
fmt.Println(slice2)

fmt.Println("切片的长度为"len(slice1))
fmt.Println("切片的容量为"cap(slice1))
// output
[1 ]
[ ]
切片的长度为 2
切片的容量为 3

切片索引

1.切片的起始位置省略,结束位置省略。则默认从数组的开始位置截取到数组的结束位置+1。得到的是和数组内容一样的切片,表示原切片。

// 切片定义省略开始和结束位置
array := []string{"A","B","C","D","E","F","G","H","I","G","K","L"}
fmt.Println("开始位置和结束位置都缺省",array[:])
// output
开始位置和结束位置都缺省 [A B C D E F G H I G K L]

2.切片的起始位置不省略,结束位置不省略。则根据起始位置和结束位置进行切取。

// 起始位置和结束位置都不省略
array := []string{"A","B","C","D","E","F","G","H","I","G","K","L"}
slice := array(0:5)
// output
[A B C D E]

3.起始位置省略,结束位置不省略。则默认从数组的最开始位置切取,直到结束位置为止。

// 起始位置省略,结束位置都不省略
array := []string{"A","B","C","D","E","F","G","H","I","G","K","L"}
slice := array(:5)
// output
[A B C D E]

4.起始位置不省略,结束位置省略。则默认从数组的指定起始位置窃取到数组的最后以为(位置为数组长度+1)。

// 起始位置不省略,结束位置省略
array := []string{"A","B","C","D","E","F","G","H","I","G","K","L"}
fmt.Println("缺省结束位置:",array[2:])
// 打印结果为
缺省结束位置: [C D E F G H I G K L]

5.切片的起始位置和结束位置,不能超出数组的范围。

array := []string{"A","B","C","D","E","F","G","H","I","G","K","L"}
fmt.Println("切片",array[-1:100])
// 打印结果为
invalid slice index -1 (index must be non-negative)

6.切片的起始位置和结束位置都为0,得到一个空的切片,表示清空切片。一般用于切片的复位。

// 起始位置和结束位置都为0
array := []string{"A","B","C","D","E","F","G","H","I","G","K","L"}
fmt.Println("切片",array[0:0])
// 打印结果为
切片: []

直接声明切片

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

定义语法

// 也可以通过一个空的数组形式
var slice []type

1.slice是切片的名称。

2.type是切片的数据类型。

代码示例

// 声明一个整型切片
var slice1 []int
// 初始化一个切片
var slice2 []int = []int{}

使用make定义切片

除了上面的几种方式外,如果需要动态地创建一个切片,可以使用 make() 内建函数。

定义语法

make([]type, size, cap)

1.type为切片的数据类型。

2.size为切片的大小。

3.cap为切片的容量。

切片的大小不能超过容量,容量表示该切片最大的元素个数,切片的大小表示实际的元素个数。例如,一个教室里面可以坐到30个人,现目前坐了10个人。这里的10就表示size,30就表示cap。

代码示例

// 定义切片
slice1 := make([]string23)
slice1[0] = "1"
fmt.Println(slice1)
// 打印如下结果
[1 ]

如果切片在复制的过程中,对应的下标未分配值,则根据数据类型默认分配一个值。例如上面的slince1定义的时2个长度,但是只给下标为0的分配了值,因此下标为1的根据数据类型时string类型,默认分配一个" "值。

常用操作

长度计算

切片长度使用len()计算。

// 计算切片长度
slice1 := make([]string23)
fmt.Println("切片的长度为"len(slice1))
// 打印结果为
切片的长度为 2

容量计算

切片容量使用cap()计算。

// 计算切片长度
slice1 := make([]string23)
fmt.Println("切片的长度为"cap(slice1))
// 打印结果为
切片的容量为 3

判断是否为空

在创建变量章节提到,变量如果创建时未给一个初始化值,编译时会默认分配一个nil的值。因此判断一个切片为空,直接与nil比较。

// 判断空切片
slice3 := make([]string23)
if slice3 == nil {
 fmt.Println("slice3是空切片")
else {
 fmt.Println("slice3不是空切片")
}

var slice4 []string
if slice4 == nil {
 fmt.Println("slice4是空切片")
else {
 fmt.Println("slice4不是空切片")
}
// output
slice3不是空切片
slice4是空切片
// 错误演示
slice1 := make([]int25)
slice2 := make([]int25)
if slice1 == slice2 {
 fmt.Println("相等")
else {
 fmt.Println("不想等")
}
// output
slice1 == slice2 (slice can only be compared to nil)

使用make创建切片时,因为定义了一个长度为2,容量为3的切片。虽然切片内容是[  ],但是实际是有值的,只不过是一个空值。切片是动态结构,只能与 nil 判定相等,不能互相判定相等。声明新的切片后,可以使用 append() 函数向切片中添加元素。

切片追加

追加的定义

使用append()可以动态的给切片的开始位置,结束位置或者中间位置添加元素。

语法格式

append(slice, element)

1.slice,要追加的切片,必须是一个切片。

2.element,向切片中添加的元素,可以是单个元素、多个元素或着切片。

尾部追加

// 切片的开始位置追加元素
var slice []int = []int {1,2,3}

// 打印原始切片的长度和容量
fmt.Println("原始切片长度和容量分别是", len(slice), cap(slice))

// 向切片后面追加一个元素
slice = append(slice, 1)
fmt.Println(slice)

// 向切片后面追加多个元素
slice = append(slice, 6,7,8,9)
fmt.Println(slice)

// 打印新的切片的长度和容量
fmt.Println("新切片长度和容量分别是", len(slice), cap(slice))
// 打印的内容分别如下
原始切片长度和容量分别是 3 3
[1 2 3 1]
[1 2 3 1 6 7 8 9]
新切片长度和容量分别是 8 12

注意事项

1.在切片的尾部添加元素,只能是单个元素或者是多个","隔开的元素,而不能是其他的数据类型。

2.如果切片追加元素时,容量不够,切片会自动的扩容。自动扩容的规律是2的倍数。如下代码:

// 验证切片追加元素自动扩容
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)
}
// output
len1  cap1 pointer: 0xc0420080e8
len2  cap2 pointer: 0xc042008150
len3  cap4 pointer: 0xc04200e320
len4  cap4 pointer: 0xc04200e320
len5  cap8 pointer: 0xc04200c200
len6  cap8 pointer: 0xc04200c200
len7  cap8 pointer: 0xc04200c200
len8  cap8 pointer: 0xc04200c200
len9  cap16 pointer: 0xc042074000
len10  cap16 pointer: 0xc042074000

开始位置追加

// 向切片的开始位置追加元素
var slice = []int {1,2,3}
slice = append([]int {0}, slice...) // 在开头添加只有1个元素的切片
slice = append([]int {-3,-2,-1}, slice...) // 在开头添加拥有多个元素的切片
fmt.Println(slice)
// output
[-3 -2 -1 0 1 2 3]

1.在切片的开始位置添加元素,将添加的元素作为append()的第一个参数,第二个参数为原始的切片,需要在原始切片后加"..."。

2.append()的第一个参数必须是切片。

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

中间位置追加

// 向切片的中间追加元素
var slice2 = []int {1,2,3,7,8,9}
slice2 = append(slice2[0:3], append([]int {4,5,6},slice2[3:]...)...)
fmt.Println(slice2)
// output
[1 2 3 4 5 6 7 8 9]

1.向切片的中间追加元素基本格式为append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x

2.每个添加操作中的第二个 append 调用都会创建一个临时切片,并将 a[i:] 的内容复制到新创建的切片中,然后将临时创建的切片再追加到 a[:i] 中。

复制

复制的定义

语言的内置函数, copy()可以将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。

copy( destSlice, srcSlice []T) int

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

示例代码

slice1 := []int{12345}
slice2 := []int{543}
copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
fmt.Println(slice2)
// output
[1 2 3]
slice1 := []int{12345}
slice2 := []int{543}
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置
fmt.Println(slice1)
// output
[5 4 3 4 5]

虽然通过循环复制切片元素更直接,不过内置的 copy() 函数使用起来更加方便,copy() 函数的第一个参数是要复制的目标 slice,第二个参数是源 slice,两个 slice 可以共享同一个底层数组,甚至有重叠也没有问题。示例代码如下:

// 通过循环的方式演示切片
slice1 := make([]int2110)
slice2 := make([]int2110)
slice1[0] = 1
slice1[1] = 2
slice2[0] = 5
fmt.Println(slice1)// [1 2]
fmt.Println(slice2)// [5 0]


// 将切片1复制到切片2
for i := 0; i < 2; i++ {
  slice2[i] = slice1[i]
}
fmt.Println(slice2)// [1 2]
// 将切片2复制到切片1
for i := 0; i < 2; i++ {
  slice1[i] = slice2[i]
}
fmt.Println(slice1)// [5 0]

引用和复制

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])
    }
}

执行逻辑 第 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 元素。

切片的复制,是在内存另外的分配,将被分配的空间分配到目标空间。原空间发生变化,新分配的空间则不会受影响。切片的引用则会收到影响。

切片的删除

切片本身不带删除的函数操作。只能使用切片自身的特性来进行操作。删除切片有如下三中情况,删除开头,删除结尾,删除中间。

删除开头

// 删除切片开头元素
// 1.使用切片的截取方法
slice = []int{123}
slice = slice[1:] // 删除开头1个元素
slice = slice[N:] // 删除开头N个元素

// 2.使用切片中的append()函数
slice = []int{123}
slice = append(slice[:0], slice[1:]...) // 删除开头1个元素
slice = append(slice[:0], slice[N:]...) // 删除开头N个元素

// 3.使用切片的copy()函数
slice = []int{123}
slice = slice[:copy(slice, slice[1:])] // 删除开头1个元素
slice = slice[:copy(slice, slice[N:])] // 删除开头N个元素

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

删除中间

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

删除结尾

// 删除结尾
slice = []int{123}
slice = slice[:len(slice)-1// 删除尾部1个元素
slice = slice[:len(slice)-N] // 删除尾部N个元素

指定位置

// 删除切片的指定位置
 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)

排序

// 整型排序
sli := []int{1534}
sort.Ints(sli)
for index, value := range sli {
fmt.Println(index, value)
}
fmt.Println("---------------")
// 字符串排序
sliStr :=[]string{"lisi""zhangsan""bruce"}
sort.Strings(sliStr)
fmt.Println(sliStr)
fmt.Println("---------------")
// 浮点型排序
sliFloat := []float64{12.5612.12}
sort.Float64s(sliFloat)
fmt.Println(sliFloat)
// output
0 1
1 3
2 4
3 5
---------------
[bruce lisi zhangsan]
---------------
[12.12 12.56]

迭代器

Go语言有个特殊的关键字 range,它可以配合关键字 for 来迭代切片里的每一个元素,如下所示:

// 创建一个整型切片,并赋值
slice := []int{10203040}
// 迭代每一个元素,并显示其值
for index, value := range slice {
    fmt.Printf("Index: %d Value: %d\n", index, value)
}
// output
Index: 0 Value: 10
Index: 1 Value: 20
Index: 2 Value: 30
Index: 3 Value: 40



浏览 35
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报