Go语言入门经典:数组与切片详解
数组和切片是Go语言中两种重要的数据结构。数组是一种固定长度的集合,而切片则是一种灵活的动态集合。本章将详细讲解数组和切片的定义、初始化、访问元素、动态操作等内容,帮助读者全面掌握这两种数据结构。
1 数组
数组是一种集合,它将一定数量且类型相同的对象放到一起,形成一个整体。数组在定义时就会确定元素的个数,初始化之后无法更改元素数量。
1.1 数组的初始化
数组实例可以使用以下几种格式来初始化:
var x [4]byte
var x = [n]T{...}
x := [n]T{...}
其中,n
表示元素的个数,即数组对象的长度。n
是一个表达式,其计算结果必须是 int
类型的常量,而且不能出现负值。
如果数组在声明时没有进行初始化,就会为每个元素分配一个默认值。例如:
var f [5]uint32{18, 75, 42, 3, 105}
变量 f
初始化后,数组中第一个元素为 18
,第二个元素为 75
,第三个元素为 42
,依此类推。
如果只想为第一个元素赋值,其他元素保留默认值(即 0
),那么初始化语句可以进行简化:
var f = [5]uint32{18,}
也可以定义变量后,通过元素索引来逐个赋值。元素索引从 0
开始,即第一个元素的索引为 0
,第二个元素的索引为 1
,依此类推。
var r [4]float32
r[0] = 1.112
r[1] = 0.000054
r[2] = 370.303
r[3] = -16.75
按照语法规则,数组变量在定义时已确定类型,数组中所有元素都必须是同一类型。因此,下面代码所示的初始化方式会发生错误:
a := [2]uint{1.7, 33}
数组变量 a
的元素类型被声明为 uint
(无符号整数),而初始化时第一个元素的值是浮点数值,与数组所定义的类型不符。
不过,如果将数组变量声明为空接口(interface{}
)类型,那么其中的元素就可以是任意类型的值了。
s := [3]interface{}{"abc", 887, 'H'}
这说明接口的动态类型机制也适用于数组。例如:
type music interface {play()pause()
}type popMusic struct {}
func (x popMusic) play() {fmt.Println("开始播放流行音乐")
}
func (x popMusic) pause() {fmt.Println("暂停播放流行音乐")
}type classicMusic struct {}
func (x classicMusic) play() {fmt.Println("开始播放古典音乐")
}
func (x classicMusic) pause() {fmt.Println("暂停播放古典音乐")
}func main() {// 初始化数组实例var arr = [2]music{popMusic{}, classicMusic{}}// 调用数组实例中各个元素的方法arr[0].play()arr[0].pause()arr[1].play()arr[1].pause()
}
在 main
函数中,数组变量 arr
的长度为 2
,并且指定元素类型为 music
接口。由于接口类型的兼容性,在初始化数组实例时可以使用 popMusic
结构体或者 classicMusic
结构体。
在数组初始化时也可以不指定长度,通过元素个数自动确定数组长度。例如:
r := [...]int32{800, 500, 1600, 2400, 900, 700}
根据所赋值的元素个数,自动推断出数组长度为 6
,即 [6]int32
。
1.2 访问数组元素
通过索引可以随机访问数组元素,索引值必须为 int
类型数值,不能是负值。索引范围为 [0, n-1]
(n
为数组长度)。
下面示例中,创建了一个包含 5
个元素的数组实例,然后通过索引读取最后两个元素:
var x = [5]rune{'a', 'e', 'i', 'o', 'u'}
// 倒数第二个元素,索引为 3
last1 := x[3]
// 最后一个元素,索引为 4
last2 := x[4]
fmt.Printf("最后两个元素: %c, %c\n", last1, last2)
最后两个元素的索引分别为 3
和 4
。此示例还可以这样处理:
var x = [5]rune{'a', 'e', 'i', 'o', 'u'}
// 获取数组的长度
n := len(x)
// 最后一个元素的索引为 n-1,倒数第二个的索引为 n-2
last1 := x[n-2]
last2 := x[n-1]
fmt.Printf("最后两个元素: %c, %c\n", last1, last2)
len
是内置函数,其作用是获得数组的长度。随后,最后两个元素的索引可以由 n
的值来确定。
上述示例的运行结果如下:
最后两个元素: o, u
如果想顺序访问数组中的所有元素,可以使用 for
循环。
第一种格式是使用带三个子句的 for
循环,通过一个临时变量来存储索引值。例如:
arr1 := [4]float32{0.11, 0.23, 5.001, 12.63}
for i := 0; i < len(arr1); i++ {fmt.Println(arr1[i])
}
初始化子句将变量 i
的值设置为 0
,可访问数组中第一个元素。执行循环的条件子句指定 i
的值应小于数组的长度(即最大值为 n-1
),每一轮循环后将变量 i
的值加 1
。
第二种格式是与 range
子句一起使用。
arr2 := [3]string{"zh-CN", "en-US", "zh-TW"}
for index, value := range arr2 {fmt.Printf("索引: %d, 值: %s\n", index, value)
}
range
子句从数组中取出一个子项,其中包含两个值——元素的索引和元素的值。
1.3 [n]T
与 *[n]T
的区别
[n]T
与 *[n]T
这两种声明格式看起来很像,但它们的含义是完全不同的。
*[n]T
:指针类型,存放类型为[n]T
的实例内存地址。[n]*T
:数组类型,其元素类型为指向int
数值的指针类型(*int
)。
可以通过两个简单的示例来说明。先看第一个示例,定义数组变量 d
,元素类型为 float32
,数组长度为 3
。
var d = [3]float32{0.001, 0.002, 0.003}
再定义变量 pd
,赋值时通过取地址运算符 &
获取数组实例的内存地址。返回的类型是 *[3]float32
。
var pd = &d
变量 pd
是指针类型,它的值是数组实例 d
的内存地址。
下面是第二个例子。定义三个变量并初始化,类型都是 int
。
var a, b, c = 50, 60, 70
接着定义变量 ax
,初始化时引用上述三个变量的内存地址。
var ax = [3]*int{&a, &b, &c}
变量 ax
为数组类型,它的元素是 *int
类型。
1.4 多维数组
多维数组指的是维度为二或二以上的数组。Go语言的多维数组更像是“数组的数组”,例如,A数组中包含元素 B,而元素 B 本身也是一个数组。
二维数组的表示形式为:
[m][n]Type
相当于:
[m]([n]Type)
三维数组的表示形式为:
[m][n][o]Type
相当于:
[m]([n]([o]Type))
读取或修改多维数组的元素,也可以通过索引来完成。
a[m][n] = x
y = a[x][y][z]
下面代码分别演示了二维数组和三维数组的使用。
// 二维数组
var a = [2][4]uint8{{12, 13, 14, 15},{16, 17, 18, 19},
}// 输出各元素
fmt.Println("----- 二维数组中的元素 -----")
for i := 0; i < 2; i++ {for j := 0; j < 4; j++ {fmt.Printf("%d ", a[i][j])}fmt.Println() // 换行
}
fmt.Println()// 三维数组
var b = [5][4][3]int32{{{1, 2, 3},{7, 8, 9},{12, 15, 18},{25, 26, 27},},{{-2, -3, -6},{-20, 35, -7},{60, 62, 64},{-100, -101, -102},},{{65, 66, 67},{305, 405, 505},{125, 135, 145},{-6, -17, 810},},{{2200, 130, -96},{-72, 160, 400},{215, -76, -320},{57, 58, 59},},{{8850, 3756, 418},{-600, -520, 307},{2125, 1102, -4720},{-595, -116, 907},},
}fmt.Println("----- 三维数组中的元素 -----")
for i := 0; i < 5; i++ {for j := 0; j < 4; j++ {for k := 0; k < 3; k++ {fmt.Printf("%d ", b[i][j][k])}fmt.Println() // 换行}fmt.Println("\n") // 换行
}
运行代码后,得到结果如下:
----- 二维数组中的元素 -----
12 13 14 15
16 17 18 19----- 三维数组中的元素 -----
1 2 3
7 8 9
12 15 18
25 26 27-2 -3 -6
-20 35 -7
60 62 64
-100 -101 -10265 66 67
305 405 505
125 135 145
-6 -17 8102200 130 -96
-72 160 400
215 -76 -320
57 58 598850 3756 418
-600 -520 307
2125 1102 -4720
-595 -116 907
2 切片
切片(slice
)与数组类似,但要比数组灵活,可以在运行阶段动态地添加元素,在实际开发中会用得比较多。
切片类型的底层是通过数组来存储元素的。这个数组实例既可以是代码中已经定义的,也可以由应用程序隐式产生的。
2.1 创建切片实例
以下几种方法都可以创建切片实例:
- 从现有的(代码中已定义过的)数组实例中“截取”出新的切片实例。格式如下:
s := a[L:H]
数组实例 a
中被提取的元素索引范围为 L <= index < H
。例如:
var x = [5]int32{2, 4, 6, 8, 10}
s := x[2:4]
变量 x
为数组对象,共 5
个元素,切片对象 s
从 x
中提取索引为 2
和 3
的元素(即第三、第四个元素),所以 s
中包含的元素为 6
和 8
。
如果将上述代码做以下修改,那么 s2
中就包含 6
、8
、10
三个元素。
s2 := a[2:5]
索引读取范围为 2 <= index < 5
,即被使用的索引为 2
、3
、4
。
将 L
和 H
两个值省略,表示使用数组中的所有元素。
s3 := a[:]
从同一个数组实例产生的所有切片实例都会共享数组中的元素,也就是说,当数组中的元素被更改,切片中对应的元素也会同步更新;反过来,如果切片中的元素被更改,数组中对应的元素也会同步更新。以下示例代码将说明这一点。
// 实例化一个数组对象
src := [4]uint32{10, 20, 30, 40}
// 从数组产生两个切片实例
s1 := src[0:2]
s2 := src[1:4]fmt.Println("----- 修改数组前 -----")
fmt.Printf("数组: %v\n", src)
fmt.Printf("切片 1: %v\n", s1)
fmt.Printf("切片 2: %v\n", s2)// 修改数组中的元素
src[0] = 100
src[2] = 300
fmt.Println("\n----- 修改数组后 -----")
fmt.Printf("数组: %v\n", src)
fmt.Printf("切片 1: %v\n", s1)
fmt.Printf("切片 2: %v\n", s2)// 修改切片中的元素
s1[1] = 700
s2[2] = 900
fmt.Println("\n----- 修改切片后 -----")
fmt.Printf("数组: %v\n", src)
fmt.Printf("切片 1: %v\n", s1)
fmt.Printf("切片 2: %v\n", s2)
数组 src
包含 4
个元素,切片 s1
使用了数组中前两个元素(索引是 0
和 1
);切片 s2
使用了第二、三、四个元素(索引为 1
、2
、3
)。这段代码的运行结果如下:
----- 修改数组前 -----
数组: [10 20 30 40]
切片 1: [10 20]
切片 2: [20 30 40]----- 修改数组后 -----
数组: [100 20 300 40]
切片 1: [100 20]
切片 2: [20 300 40]----- 修改切片后 -----
数组: [100 700 300 900]
切片 1: [100 700]
切片 2: [700 300 900]
数组中的第一个元素被修改为 100
,切片 s1
的第一个元素也同步更新为 100
;同理,数组中第三个元素被修改为 300
,切片 s2
的第二个元素也同步更新为 300
。对切片实例的修改也会同步到数组实例上,因为切片 s1
、s2
都是以数组 src
为存储基础的,它们共享数组中的元素。
- 直接初始化。格式与数组接近,示例如下:
var (s = []string{"how", "do", "you", "do"}t = []float64{999.0000065, -73.30000082}
)
切片的初始化表达式中不需要指定元素个数(长度),但一对空白中括号([]
)必须保留。
- 使用
make
函数。
s := make([]byte, 30)
第一个参数指定要创建实例的类型,此处必须指明是切片类型。因为 make
函数不仅可以创建切片(slice
)实例,也可以创建通道(channel
)、映射(map
)实例。第二个参数指定切片的长度。上述代码中,创建了一个长度为 30
的切片,而且每个元素都会使用 byte
类型的默认值来初始化。
2.2 添加和删除元素
向切片添加元素,可以调用 append
函数。函数原型如下:
func append(slice []Type, elems ...Type) []Type
slice
参数是要追加元素的切片实例,elems
是个数可变的参数,它表示要添加到切片实例中的元素,可以是一个元素,也可以是多个元素。
如果切片所引用的基础数组有足够的容量容纳新添加的元素,那么 append
函数将原来的切片实例返回;如果基础数组的容量不足,append
函数会创建新的数组实例并分配更大的空间,然后把旧数组实例的元素复制到新实例中,并添加新的元素,最后返回由新数组实例所产生的切片实例。
在调用 append
函数前,代码不需要验证切片的容量是否足够,因为 append
函数会自动处理。但是,为了在调用 append
函数后能够获得最新的切片实例,一般会把 append
函数返回的实例重新赋值给切片类型的变量。就像下面这样:
s = append(s, ...)
下面请看一个示例。
先定义一个函数,用来向屏幕输出切片实例的长度与容量。
func printSliceInfo(s []float32) {fmt.Printf("长度:%d, 容量:%d, 元素列表:%v\n", len(s), cap(s), s)
}
len
函数获取的是切片实例的长度,cap
函数获取的是切片实例的容量,长度是指切片中可以被访问的元素个数,而容量是指应用程序为切片的基础数组所分配的空间。为了保证有足够的空间,容量必须大于或等于长度。
初始化一个切片实例,它包含两个元素。然后多次调用 append
函数向切片实例添加元素。
var sf = []float32{0.001, 0.0007}
printSliceInfo(sf)
// 添加一个元素
sf = append(sf, 0.0014)
printSliceInfo(sf)
// 添加两个元素
sf = append(sf, 0.0008, 0.1205)
printSliceInfo(sf)
// 添加三个元素
sf = append(sf, 0.0275, 1.302, 5.0071)
printSliceInfo(sf)
得到的输出结果如下:
长度:2, 容量:2, 元素列表:[0.001 0.0007]
长度:3, 容量:4, 元素列表:[0.001 0.0007 0.0014]
长度:5, 容量:8, 元素列表:[0.001 0.0007 0.0014 0.0008 0.1205]
长度:8, 容量:8, 元素列表:[0.001 0.0007 0.0014 0.0008 0.1205 0.0275 1.302 5.0071]
如果使用 make
函数来创建切片实例,可以为其设置一个默认的容量(初始容量)。当然,随着元素的添加,容量会自动增长。
var s = make([]string, 0, 10)
fmt.Printf("初始化后,长度:%d, 容量:%d\n", len(s), cap(s))
// 添加 50 个元素
for i := 1; i <= 50; i++ {str := fmt.Sprintf("Item %d", i)s = append(s, str)
}
fmt.Printf("添加 50 个元素后,长度:%d, 容量:%d\n", len(s), cap(s))
切片实例 s
初始化的容量为 10
,长度为 0
,即基础数组分配了可容纳 10
个元素的空间,但其中包含元素个数为 0
。如果将 make
函数的调用代码做以下修改,那么创建的切片实例中已包含 3
个元素,这 3
个元素都分配了 string
类型的默认值(空字符串)。
var s = make([]string, 3, 10)
标准库没有提供用于删除切片元素的函数,但是,可以通过截取元素来实现。例如:
var s = []int{1, 2, 3, 4, 5}
fmt.Printf("初始元素列表:%v\n", s)// 截取除最后一个元素外的所有元素
s = s[0 : len(s)-1]
fmt.Printf("删掉最后一个元素后:%v\n", s)
上面代码中,切片实例 s
有 5
个元素,截取时从索引 0
开始,到 len(s)-1
,即 [0:4]
,这样一来,被提取到新切片实例的元素为 1
、2
、3
、4
,删除最后一个元素的目的便实现了。
输出结果如下:
初始元素列表:[1 2 3 4 5]
删掉最后一个元素后:[1 2 3 4]
下面的例子将演示如何删除切片实例中的前两个元素。
var s = []int{1, 2, 3, 4, 5}
fmt.Printf("初始元素列表:%v\n", s)
// 删除前两个元素
s = s[2:]
fmt.Printf("删除前两个元素后:%v\n", s)
[2:]
表示从索引 2
(第三个元素)开始截取,直到最后一个元素。这样一来就删除了前两个元素,结果如下:
初始元素列表:[1 2 3 4 5]
删除前两个元素后:[3 4 5]
总结
通过本章的学习,读者应该对Go语言中的数组和切片有了全面的了解。数组提供固定长度的集合,适合需要明确边界的数据结构;而切片则提供了灵活的动态集合,适合需要动态扩展的数据结构。在实际开发中,根据需求选择合适的结构,可以提高代码的效率和可维护性。希望本章的内容能够帮助读者在Go语言的学习和应用中取得更大的进步。