一、Go中的数组
1、数组的声明
Go中的数组表示为T[n]的形式,表示长度为n的数组,下标从0开始到n-1。常见的数组声明方式有三种,分别如下:
第一种:
var 数组名 [数组长度]数组元素类型
示例如下:
import "fmt" func main(){ var a [4]int fmt.Println(a) }
这样就声明了一个长度为4的int型数组a,默认初始值为0,输出结果就是[0 0 0 0]
第二种(简写声明,用:=,同时可完成数组的赋值,不赋值则为默认值0):
数组名 := [数组长度]数组元素类型{元素1,元素2,…}
示例如下:
import "fmt" func main(){ a := [4]int{} fmt.Println(a) }
效果和第一种是一样的.
第三种(用“...”代替数组长度,让编译器自动计算,如果不进行赋值,则为一个长度为0的数组):
数组名 := [...]数组元素类型{元素1,元素2,…}
示例如下:
import "fmt" func main(){ a: = [...]int{} fmt.Println(a,len(a)) b: = [...]int{1,2,3} fmt.Println(b,len(b)) }
输出就为:
[] 0
[1 2 3] 3
2、数组的使用说明
1)数组是值类型
Golang中数组是值类型,而不是引用类型,这意味着把数组赋值给一个新的变量时,新变量会得到一个原始数组的副本,而非原始数组的引用(指针),故修改新数组不会导致原始数组变化。
import "fmt"
func main(){
a := [...]int{1,2,3}
b := a
b[0] = 0
fmt.Println(a)
fmt.Println(b)
}
上面示例代码的输出为:
[1 2 3][0 2 3]
2)使用for-range遍历数组
Golang中提供了一种方便简洁的数组遍历方法,即for循环的range方法来遍历数组。
range方法返回索引和该索引处的值,见如下代码:
import "fmt"
func main(){
a :=[...]int{1,2,3}
for i, v := range a{
fmt.Printf("a[%d] = %d\n", i, v)
}
}
上面示例代码的输出为:
a[0] = 1
a[1] = 2
a[2] = 3
如果只需要值,而忽略索引的话,可以用空白标识符 _ 来替换对应位置的变量即可。
for _, v := range a{
}
3)多维数组
Golang也支持多维数组的声明与使用,例如我们声明一个4行2列的二维数组就可以这么写:
import "fmt"
func main(){
a := [4][2]int{
{1, 2},
{3, 4},
{5, 6},
{7, 8},
}
fmt.Println(a)
}
上面代码的输出为:
[[1 2] [3 4] [5 6] [7 8]]
多维数组声明时,第一维的长度也可以用“...”来代替,编译器会自动计算长度,但后面维度的长度需要显示的声明,否则会报错;同样,三维数组第一维可以用"..."替代,但第二、第三维就不可以,以此类推。
a := [...][2]int{ //这样是可以的
{1, 2},
{3, 4},
{5, 6},
{7, 8},
}
a := [...][...]int{ //报错
{1, 2},
{3, 4},
{5, 6},
{7, 8},
}
a := [4][...]int{ //报错
{1, 2},
{3, 4},
{5, 6},
{7, 8},
}
二、切片(slice)
切片是对数组一个连续片段的引用(该数组被称为底层数组,通常是匿名的),所以切片是一个引用类型。实际开发中切片更加常用,因为其动态性是数组不具备的。简而言之,切片可以理解为一个长度可以变化的数组。
可以认为切片自身有一些属性,例如切片的长度和容量,后文会介绍,以及指向引用开始元素的指针,这个指针导致了将切片传给函数时,函数内对切片的修改在函数外是可见的,即更改了对应地址的值。
1、切片的创建
具有T类型元素的切片表示为[ ]T,切片的创建方法也有三种。
第一种:
var 切片名 []切片元素类型 = 数组[begin : end]
表示创建了一个 “引用数组下标从begin到end-1元素” 的切片
示例如下:
import "fmt" func main(){ a := [5]int{1, 2, 3, 4, 5} var b []int = a[1:5] fmt.Println(b) }
上述代码输出为:
[2 3 4 5]
第二种:
切片名 := []切片元素类型{元素1, 元素2,...,元素n}表示创建了一个有n个元素的切片元素类型数组,并返回切片引用给对应切片
示例如下:
import "fmt" func main(){ a := []int{1,2,3,4} fmt.Println(a) }
上述代码输出为:
[1 2 3 4]
第三种(使用make函数):
make函数定义为:
func make ([]T, len, cap) []T
其中len和cap即为之前提到过的切片长度和容量(接下来马上介绍这两个概念),通过传递类型、长度和容量来创建切片,容量的默认值是切片长度。
make函数的效果是创建一个数组,然后返回引用该数组的切片。
示例如下:
import "fmt" func main(){ a := make([]int, 4, 4) fmt.Println(a) }
上面代码输出为:
[0 0 0 0]make函数创建切片时默认元素值为0
2、切片的长度和容量
切片的长度是指切片中的元素数,切片的容量是指从船舰切片索引开始算起,底层数组中元素的个数。可以用len()函数和cap()函数来返回切片的长度和容量,我们用下面一段代码来理解切片的长度和容量。
import "fmt"
func main(){
a := [...]float64{1.2, 2.3, 3.4, 4.5, 5.6, 6.7}
sliceA := a[2:4]
fmt.Println(sliceA)
fmt.Printf("length = %d, capacity = %d", len(sliceA), cap(sliceA))
}
上面代码的输出为:
[3.4 4.5]
length = 2, capacity = 4
长度很好理解,切片sliceA中有两个元素,故长度为2,但是容量是如何算出来的呢?我们更改代码再次看一下结果:
import "fmt"
func main(){
a := [...]float64{1.2, 2.3, 3.4, 4.5, 5.6, 6.7}
sliceA := a[3:5]
fmt.Println(sliceA)
fmt.Printf("length = %d, capacity = %d", len(sliceA), cap(sliceA))
}
上面代码的输出为:
[4.5 5.6]
length = 2, capacity = 3
发现容量发生了变化,我们可以看出,在这里,容量可以通俗的视作从切片开始下标在底层数组中的元素开始,到底层数组最后一个元素共计的元素个数。例如第一个例子中,我们从下标为2的元素开始切片,则容量为6-2=4,第二个例子中,我们从下标为2的元素开始切片,则容量为6-3=3。
切片的长度和容量是可以动态变化的,在追加切片元素时可以明显观察到。
3、追加切片元素与动态变化
追加切片元素我们使用append函数,其函数定义如下:
func append([]T, x ... T) T[]
x ... T在函数定义中表示该函数接受参数x的个数是可变的。这类函数被称为可变参函数。
Golang实现切片动态变化的方法十分“简单粗暴”。
当新的元素被添加至切片时,切片的长度会增加,增加的数量为新元素个数。长度增加后,如果当前切片的容量小于增加以后的切片长度,则切片会扩容,切片的容量会翻一倍(2x原理来怎加数组长度),此时底层会自动创建一个扩容后的新数组,然后将原数组的值复制给新数组,并添加追加的元素,随后返回该数组的切片引用,不会影响原来的底层数组;若大于,则容量不变,切片继继续引用当前数组,并且修改底层数组对应位置元素。
我们用代码来说明。
import "fmt"
func main() {
a := [...]float64{1.2, 2.3, 3.4, 4.5, 5.6, 6.7}
sliceA := a[2:4]
sliceA = append(sliceA, 7.8, 8.9, 9.1)
sliceA[0] = 1.1
fmt.Printf("切片A为")
fmt.Println(sliceA)
fmt.Printf("length = %d, capacity = %d\n当前原数组为:", len(sliceA), cap(sliceA))
fmt.Println(a)
sliceB := a[2:4]
sliceB = append(sliceB, 7.8, 8.9)
sliceB[0] = 1.1
fmt.Printf("切片B为")
fmt.Println(sliceB)
fmt.Printf("length = %d, capacity = %d\n当前原数组为:", len(sliceB), cap(sliceB))
fmt.Println(a)
}
上述代码的输出为:
可以发现,追加元素时:
当切片扩容,切片的修改并不会影响原底层数组的数据;而切片不扩容时,切片将继续引用原底层数组,所以数组的数据发生了改变。
这便于我们理解切片动态变化的原理。
这一块可能有点难理解,可以上机实操辅助理解。
尝试以下代码,可以更深刻的理解切片与底层数组的关系。
func main() {
a := [...]float64{1.2, 2.3, 3.4, 4.5, 5.6, 6.7}
sliceA := a[2:4]
sliceA = append(sliceA, 7.8, 8.9, 9.1)
sliceA[0] = 1.1
sliceA = sliceA[:cap(sliceA)] //我们看一看新的底层数组未被引用部分是怎样的
fmt.Printf("切片A为")
fmt.Println(sliceA)
}
上面代码的输出为:
可以看到切片A的后面三个元素都是0,可以知道新的底层数组在未追加元素的情况下,默认值为0
4、多维切片
与数组类似,切片也可以有多个维度。
import "fmt"
func main(){
sliceA = [][]int{
{1, 2},
{3},
{4, 5},
}
fmt.Println(sliceA, len(sliceA), cap(sliceA))
}
上面代码的输出为:
[[1 2] [3] [4 5]] 3 3