Go语言学习 十五 数组和切片
数组是一种重要的数据结构,一旦声明长度就是固定的,实际中,更为灵活的切片可能使用更为广泛。切片底层实现就是使用的数组。
一 数组
数组是一种长度固定的数据结构,一旦声明,长度和元素的类型就不可变,并且在内存中是一段连续的块。数组中元素的个数即为数组的长度。数组的长度是一个int
类型,可以使用内置函数len()
来获取。另外,数组中的每一个元素都对应一个索引(下标),索引是一个整数,从0
开始。对于一个数组a
,其长度为len(a)
,可以使用索引0-len(a)-1
来访问数组中的每一个元素。
数组长度不能为负,索引也不能为负
1.1 数组的声明
数组本身是一维类型的,但是可以将数组进行多层嵌套从而构造出多维数组,即数组的元素本身又是一个数组。
1.1.1 使用关键字var声明
var 数组名称 [长度]元素类型
示例:
var arr [3]int // 声明一个长度为3,元素类型为int的数组
var x [16]byte // 声明一个长度为16,元素类型为byte的数组
var p [2][2]float64 // 声明一个2x2的浮点二维数组,等价于[2]([2]float64)
数组在声明时,会使用每个元素对应的零值来初始化数组。对于整型数组,每个元素的初始值均为0。
1.1.2 使用数组字面量声明
数组名 := [长度]元素类型{元素0, 元素1, ..., 元素length-1}
示例:
arr := [4]int{1, 3, 4, 5} // 声明一个长度为4,且每个元素都初始化为具体的值
另外,还可以使用...
代替长度,让Go根据初始化时的元素个数来确定其长度:
array := [...]float64{7.0, 8.5, 9.1} // 实际长度为3
由于数组的元素都有一个索引对应,所以也可以在声明时,指定特定元素的初始值:
// 指定索引为1,3,5的元素的初始值
// 初始化后的元素:[0 10 0 40 0 50 0 0]
arr := [8]int{1: 10, 3: 40, 5: 50}
1.2 数组的使用
1.2.1 访问数组
给定一个数组:
arr := [4]int{1, 3, 4, 5, 6}
前面提到,可以使用数组的索引来访问数组中的元素:
语法: 数组名[索引]
arr[0] // 访问第0个元素
x := arr[1] // 将第1个元素赋值给其他变量
arr[2] = 10 // 修改第2个元素的值
另外,还可以使用数组[[索引开始]:[索引结束]]
的形式来截取数组中的一部分元素,这样就形成了一个切片(参见下一小节):
a0 := arr // 数组:直接把数组arr赋值给另一个数组,达到复制数组的目的
a1 := arr[1:3] // 切片:{3, 4}
a2 := arr[2:] // 切片:等价于arr[2:4],{4, 5, 6}
a3 := arr[:3] // 切片:等价于arr[0:3],{1, 3, 4}
a4 := arr[:] // 切片:与arr 数组一样,{1, 3, 4, 5, 6}
其中,索引的开始和结束又称为上界和下界。
1.3 数组的值和引用
了解数组的工作方式很重要,尤其是在规划内存的详细布局时,可以帮助避免重新分配内存。
以下是几个关于Go数组的重要特点。
数组在Go和C中的工作方式有很大差异。在Go中:
- 数组是一个值。将一个数组分配给另一个数组会复制所有元素。
- 如果将数组传递给函数,它将接收数组的副本,而不是指向它的指针。
- 数组的大小是其类型的一部分。类型
[10]int
和[20]int
是不同的。
在函数间传递数组时要小心复制问题,这可能不是你希望的
值属性很有用,但也很昂贵;如果你想要类似C的行为和效率,你可以传递一个指向数组的指针。
func Sum(a *[3]float64) (sum float64) {
for _, v := range *a {
sum += v
}
return
}
array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array) // 注意显式的取址操作符&
但即便是这种风格也不是Go的习惯用法,应该使用切片代替。
二 切片
不同于数组,切片则为数组元素提供动态大小的、灵活的视角。在实践中,切片比数组更常用。切片包装了数组,为数据序列提供了更通用、更强大且方便的接口。除具有显示维度的条目(如转换矩阵)之外,Go中大多数数组编程都是使用切片而不是简单的数组完成的。
切片持有对底层数组的引用,如果将一个切片分配给另一个切片,则两者都引用同一个数组。如果函数使用切片参数,对切片元素所做的修改对调用者是可见的,类似于将指针传递给底层数组。
2.1 切片的声明
将数组声明中的长度去掉,就变成了切片声明:
var s []int
但是由于切片是引用类型,所以上述声明还未初始化,不能够使用,切片必须先初始化才能使用。切片的零值为nil
,nil
切片的长度和容量为0且没有底层数组。类型[]T
表示一个元素类型为T
的切片。
可以借助内置函数make()
来初始化切片:
make([]T, length) // 只指定切片长度
make([]T, length, capacity) // 指定长度和容量
make
函数会分配一个元素为零值的数组并返回一个引用了它的切片。其中,length
,capacity
均为整数,且length <= capacity
。
切片s的长度和容量满足:0 <= len(s) <= cap(s)
例如:
s := make([]int, 2, 5) // 创建一个长度为2,容量为5的整型切片
它等价于以下表达式:
s := new([5]int)[0:2]// 创建数组然后进行切片
需要注意的是,初始化好的切片只能访问长度指定的范围,其余超出长度但在容量范围内的元素并不能直接访问,还需要借助内置函数append
来操作(见下文)。
另外,还可以使用切片字面量直接初始化:
s := []int{1,3,5,7} // 创建一个长度和容量均为4的整型切片
同样,也可以使用索引来初始化切片:
s := []string{5: "", 9: "hello"} // 创建一个长度和容量均为10的切片
注意声明切片和数组的区别,切片在[ ]中没有指定长度。
切片拥有 长度
和 容量
。切片的长度就是它所包含的元素个数。切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。
切片s
的长度和容量可通过表达式len(s)
和cap(s)
来获取。你可以通过重新切片来扩展一个切片,给它提供足够的容量,前提是,不能超过其容量。
2.1.1 二维切片
与数组一样,切片也是一维的,要创建二维或多维切片,需要定义一个切片的切片:
s := [][]byte // 一个二维字节切片
多维切片内部切片的长度可以动态变化,且内部切片需要单独初始化。
2.2 切片表达式
切片表达式可以从切片、数组、字符串等构造字符串或切片,有两种变体:
2.2.1 简单切片表达式
对于字符串、数组、数组指针,或切片,可以通过2个下标来界定,即一个上界和一个下界,二者以冒号分隔:a[low : high]
它会选择一个半开区间(前闭后开),包括第一个元素,但排除最后一个元素。
a := [5]int{1, 2, 3, 4, 5}
s := a[1:4] // {2, 3, 4}
新的切片的长度为high - low
,容量为底层数组的容量 - low
。
对于上述示例,新切片s
的长度为4-1=3,容量为5-1=4。
以下表达式创建了一个切片,它包含primes中下标从 1 到 3 的元素:s[1:4]
package main
import "fmt"
func main() {
primes := [6]int{2, 3, 5, 7, 11, 13}
var s []int = primes[1:4]
fmt.Println(s)
}
在进行切片时,可以利用它的默认行为来忽略上下界。切片下界的默认值为 0,上界则是该切片的长度。
对于数组var a [10]int
来说,以下切片是等价的:
a[0:10]
a[:10]
a[0:]
a[:]
如果a
是一个指向数组的指针,则a[low : high]
是(*a)[low : high]
的简写。对于数组或字符串,下标索引需要满足0 <= low <= high <= len(a)
,否则会引起索引越界。对于切片,它的上界是它的容量cap(a)
,而不是长度。
2.2.2 完整切片表达式
对于数组、数组指针,或非字符串切片,可以通过3个下标来界定:a[low : high : max]
其中第三个下标max
用于控制新切片的容量:max - low
,只有第一个下标可以省略,默认为0:
a := [5]int{1, 2, 3, 4, 5}
t := a[1:3:5]
s := a[:3:5]
其他与简单切片表达式一样。如果a
是一个指向数组的指针,则a[low : high : max]
是(*a)[low : high : max]
的简写。如果是一个数组,则它必须是可寻址的。索引需要满足0 <= low <= high <= max <= cap(a)
,否则会引起索引越界。数组的索引也是一样。
所有有关数组和切片的下标索引都是大于0的int类型。
2.2.3 切片就像数组的引用
切片并不存储任何数据,它只是描述了底层数组中的一段。基于这一点,在函数中传递切片与传递数组并不一样,切片本身非常小,因而成本也很低。更改切片的元素会修改其底层数组中对应的元素。与它共享底层数组的切片都会观测到这些修改。
package main
import "fmt"
func main() {
names := [4]string{
"John",
"Paul",
"George",
"Ringo",
}
fmt.Println(names)
a := names[0:2]
b := names[1:3]
fmt.Println(a, b)
b[0] = "XXX"
fmt.Println(a, b)
fmt.Println(names)
}
2.3 向切片追加元素
为切片追加新的元素是种常用的操作,为此Go提供了内建的append
函数。内建函数的文档对此函数有详细的介绍。
func append(s []T, vs ...T) []T
append
的第一个参数s
是一个元素类型为T
的切片,其余类型为T
的值将会追加到该切片的末尾。append
的结果是一个包含原切片所有元素加上新添加元素的切片。
当s
的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。返回的切片会指向这个新分配的数组。
package main
import (
"fmt"
)
func main() {
s := make([]int, 2, 5)
fmt.Println(s)
s = append(s, 1, 2, 3, 4, 5)
fmt.Println(s)
}
(要了解关于切片的更多内容,请阅读文章Go切片:用法和本质。)
三 数组和切片的遍历
for
循环的range
形式可遍历数组或切片。当使用for
循环遍历切片时,每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。
package main
import "fmt"
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
func main() {
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
}
可以将下标或值赋予 _ 来忽略它:for i, _ := range pow
for _, value := range pow
若你只需要索引,忽略第二个变量即可:for i := range pow
package main
import "fmt"
func main() {
pow := make([]int, 10)
for i := range pow {
pow[i] = 1 << uint(i) // == 2**i
}
for _, value := range pow {
fmt.Printf("%d\n", value)
}
}
参考:
https://golang.org/doc/effective_go.html#arrays
https://golang.org/doc/effective_go.html#slices
https://golang.org/ref/spec#Array_types
https://golang.org/ref/spec#Slice_types
https://golang.org/ref/spec#Slice_expressions
版权声明:知识共享署名-非商用-非衍生 (CC BY-NC-ND 3.0) 转载请注明出处