Go语言中的数组和切片
复合数据类型
在计算机科学中,复合类型是一种数据类型,它可以原始类型和其他的复合类型所构成,构成一个复合类型的动作,又称为组合。
复合数据类型主要有:
①、数组
②、Slice
③、Map
④、结构体
⑤、JSON
数组和结构体都是有固定内存大小的数据结构。
在复合数据类型中数组是由同构的元素组成--每个数组元素都是完全相同的类型--结构体则是异构的元素组成的。
相比之下,slice和map则是动态的数据结构,它们将根据需要动态增长。
数组
数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。在Go中,数组是一种固定长度,类型相同的数据结构,数组可以包含任何类型的元素,但是它们的长度在定义时就必须确定,并且无法动态改变。
以下是一个数组的案例:
package mainimport "fmt"func ArrayDemo() { // 定义一个包含五个整数的数组 var a [5]int // 打印数组的值 fmt.Println(a) // 修改数组中的元素 a[2] = 3 fmt.Println(a) fmt.Println(a[2]) // 定义并初始化一个数组 b := [3]string{"hello", "world", "!"} fmt.Println(b) // 遍历数组并打印每个元素 for i, x := range b { fmt.Printf("b[%d]: %s\n", i, x) }}
执行结果如下:
[0 0 0 0 0][0 0 3 0 0]3[hello world !]b[0]: hellob[1]: worldb[2]: !
在该示例中,我们首先使用var关键字定义了一个包含五个整数的数组a,由于没有显示初始化数组中的元素,因此它们都被自动初始化零值。
我们通过fmt.println函数打印了这个数组的值,并修改了其中的一个元素,并再次打印这个数组的值和一个特定的元素。
接下来,我们使用短变量声明语法定义了一个包含三个字符串的数组b,并通过花括号进行了初始化。我们遍历了数组b并通过fmt.printf函数打印了每个元素的索引和值。
需要注意的是,在使用数组时,我们需要特别注意数组的长度和元素类型,并尽可能地遵循最佳实践,以确保代码的正确性和可读性。同时,我们也需要注意数组在内存中的分布和访问方式,以便在处理大型数据集合高性能应用程序时保持最佳实践。
数组的定义
数组的每个元素都被初始化为元素类型的零值,对于数字类型来说就是0.
var m [3]int = [3]int{1, 2, 3}var n [3]int = [3]int{1, 2}fmt.Println(n[2]) // "0"
在数组字面值中,如果在数组的长度位置出现的是"..."省略号,则表示住宿的长度是根据初始化值的个数来计算。
m := [...]int{1, 2, 3}fmt.Printf("%T\n", m) // "[3]int"
数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是两种不同的数组类型,数组的长度必须是常量表达式,因此数组的长度需要在编译阶段确定。
数组可以直接进行比较,当数组内的元素都一样的时候表示两个数组相等。
arr1 := [3]int{1, 2, 3}arr2 := [3]int{1, 2, 3}arr3 := [3]int{1, 2, 4}fmt.Println(arr1 == arr2, arr1 == arr3) //true,false
元素的访问
数组的每个元素可以通过索引下标来访问,索引下标的范围是从0开始到数组长度减1的位置。内置的len函数将返回数组中元素的个数。
数组作为参数
数组可以作为函数的参数传入,但由于数组在作为参数的时候,其实是进行了拷贝,这样在函数内部改变数组的值,是不影响到外面数组的值
func ArrIsArgs(arr [4]int) { arr[0] = 120}m := [...]int{1, 2, 3, 4}ArrIsArgs(m)
如果想要改变外部数组的值,就只能使用指针。
使用指针,在函数内部改变的数组的值,也会改变外面数组的值
func ArrIsArgs(arr *[4]int) { arr[0] = 20}m:= [...]int{1, 2, 3, 4}ArrIsArgs(&m)
这里的*和&的区别:
①、&是取地址符号,即取得某个变量的地址,如:&a
②、*是指指针类型变量定义,可以表示一个变量是指针类型,也可以表示一个指针变量所指向的存储单元,也就是这个地址所存储的值。
通常这样的情况下都是用切片来解决,而不是用数组。
由于数组的长度是固定的,因而在使用的时候我们用的最多的是slice(切片),它是可以增长和收缩动态序列,slice功能也更灵活。
Slice
Slice(切片)代表变长的序列,序列中的每个元素都有相同的类型
一个Slice类型一般写作[]T,其中T代表Slice中元素的类型
Slice的语法和数组很像,只是没有固定长度而已
数组和Slice关系非常密切,一个Slice可以访问数组的部分或者全部数据,而且Slice的底层本身就是对数组的引用。
从数组或切片生成新的切片
切片默认指向一段连续的内存区域,可以是数组,也可以是切片本身。
从连续内存区域生成切片是常见的操作,格式如下:
// slice:表示目标切片对象// 开始位置:对应目标切片对象的索引// 结束位置:对应目标切片的结束索引slice [开始位置 : 结束位置]
一个slice分为三个部分组成:指针,长度和容量
①、内置的len和cap函数可以分别返回slice的长度和容量
②、指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice第一个元素并不一定就是数组的第一个元素
③、长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice开始位置到底层数组的结尾位置,内置的len和cap函数分别返回slice的长度和容量。
一个slice的案例
在Go中,切片是一个动态长度的,可变长度数据结构,它基于数组实现,但是可以动态增加或减少其长度,并且支持各种强大的操作和函数。
以下是一个基本的切片定义和使用方式:
func SliceDemo() { // 定义一个包含五个整数的数组 a := [5]int{1, 2, 3, 4, 5} // 声明一个从数组 a 中获取的切片 s := a[1:4] // 打印切片的值和长度 fmt.Println(s) fmt.Println(len(s)) // 修改切片中的元素 s[1] = 10 fmt.Println(s) fmt.Println(a) // 使用 make 函数创建一个新的切片 b := make([]int, 3) fmt.Println(b) // 向切片中添加新的元素 b = append(b, 4, 5, 6) fmt.Println(b) // 遍历切片并打印每个元素 for i, x := range b { fmt.Printf("b[%d]: %d\n", i, x) }}
执行结果
[2 3 4]3[2 10 4][1 2 10 4 5][0 0 0][0 0 0 4 5 6]b[0]: 0b[1]: 0b[2]: 0b[3]: 4b[4]: 5b[5]: 6
在该示例中,首先定义了一个包含五个整数的数组a,并通过[1:4]的方式声明了一个从数组a中获取切片s,通过fmt.println函数打印了这个切片的值和长度,并修改了其中一个元素,查看了其对原数组的影响。
接下来,使用了make函数创建了一个新的切片b,并通过append函数向其中添加了三个新元素,遍历了切片b并通过fmt.printf函数打印了每个元素的索引和值。
从指定范围中生成切片
切片有点像C语言里的指针,指针可以做运算,但是代价是内存操作越界。
切片在指针的基础上增加了大小,约束了切片对应的内存区域,切片使用中无法对切片内部的地址和大小进行手动调整,因此切片比指针更安全,强大。
从数组生成切片,代码如下:
var a = [3]int{1, 2, 3}fmt.Println(a, a[1:2])// []int
其中a是一个拥有3个整型元素的数组,被初始化为数值1到3,使用a[1:2]可以生成一个新的切片,代码运行结果如下:
[1 2 3] [2]
其中[2]就是a[1:2]切片操作的结果
切片和数组密不可分,如果将数组理解为一栋办公楼,那么切片就是把不同的连续楼层出租给使用者, 出租的过程需要选择开始楼层和结束楼层,这个过程就会生成切片,表示原有的切片。
生成切片的格式中,当开始和结束位置都被忽略时,生成的切片将表示和原切片一致的切片,并且生成的切片与原切片在数据内容上也是一致的,代码如下:
a := []int{1, 2, 3}fmt.Println(a[:])
a是一个拥有3个元素的切片,将a切片使用a[:]进行操作后,得到的切片与a切片一样,,代码输出结果如下:
[1 2 3]
从数组或切片生成新的切片拥有如下特性:
①、取出的元素数量为:结束位置 - 开始位置;
②、取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[len(slice)] 获取;
③、当缺少开始位置时,表示从连续区域开头到结束位置;
④、当缺少结束位置时,表示从开始位置到整个连续区域末尾;
⑤、两者同时缺少时,与切片本身等效;
⑥、两者同时为 0 时,等效于空切片,一般用于切片复位。
根据索引位置取切片 slice 元素值时,取值范围是 (0~len(slice)-1) ,超界会报运行时错误,生成切片时,结束位置可以填写 len(slice) 但不会报错。下面通过实例来熟悉切片的特性:
重置切片,清空拥有的元素
把切片的开始和结束位置都设为 0 时,生成的切片将变空,代码如下:
a := []int{1, 2, 3}fmt.Println(a[0:0])
代码输出如下:
[]
直接声明新的切片
除了可以从原有的数组或者切片中生成切片外,也可以声明一个新的切片,每一种类型都可以拥有其切片类型,表示多个相同类型元素的连续集合,因此切片类型也可以被声明,切片类型声明格式如下:
// 其中 name 表示切片的变量名// Type 表示切片对应的元素类型var name []Type
下面代码展示了切片声明的使用过程:
// 声明字符串切片// 声明一个字符串切片,切片中拥有多个字符串var strList []string// 声明整型切片// 声明一个整型切片,切片中拥有多个整型数值var numList []int// 声明一个空切片// 将 numListEmpty 声明为一个整型切片// 本来会在{}中填充切片的初始化元素,这里没有填充,所以切片是空的,但是此时的numListEmpty 已经被分配了内存,只是还没有元素var numListEmpty = []int{}// 输出3个切片// 切片均没有任何元素,3 个切片输出元素内容均为空fmt.Println(strList, numList, numListEmpty)// 输出3个切片大小// 没有对切片进行任何操作,strList 和 numList 没有指向任何数组或者其他切片fmt.Println(len(strList), len(numList), len(numListEmpty))// 切片判定空的结果//声明但未使用的切片的默认值是 nil,strList 和 numList 也是 nil,所以和 nil 比较的结果是 true// numListEmpty 已经被分配到了内存,但没有元素,因此和 nil 比较时是 falsefmt.Println(strList == nil)fmt.Println(numList == nil)fmt.Println(numListEmpty == nil)
代码输出结果:
[] [] []0 0 0truetruefalse