Go语言系列之数组和切片

Posted xingrenguanxue

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go语言系列之数组和切片相关的知识,希望对你有一定的参考价值。

《Go语言系列文章》

  1. Go语言系列(一)之Go的安装和使用
  2. Go语言系列(二)之基础语法总结

1. 数组

数组用于存储若干个相同类型的变量的集合。数组中每个变量称为数组的元素,每个元素都有一个数字编号——数组下标,该下标从0开始,用于区别各个元素。数组中可容纳的元素个数称为数组的长度

1.1. 声明

Go语言中数组的声明方式:

var arr_name [length]type

var:不必多说,声明变量时都会用到该关键字。

arr_name:数组名称,本质是个变量

length:数组的长度

type:数组的类型

[]:通过它来进行对数组元素的读取、赋值

下面是一个例子:

package main

import "fmt"

func main() {
    var a [2]string //声明一个长度为2的string数组
    a[0] = "我是"	   //赋值
    a[1] = "行小观"
    fmt.Println(a[0], a[1]) //获取元素
    fmt.Println(a)
}

1.2. 初始化

《Go语言系列(二)之基础语法总结》这篇文章中提过:若我们在声明变量时,不给变量赋初始值,则这些变量会被赋予“零值”。

数组中也是这样,如果不初始化,则数组中的所有元素值都为“零值”。如下例:

package main

import "fmt"

func main() {
    var a [3]int
    var b [3]string
    var c [3]bool

    fmt.Println(a) //[0 0 0]
    fmt.Println(b) //[  ]
    fmt.Println(c) //[false false false]
}

对数组元素进行初始化:

package main

import "fmt"

func main() {
    var a = [5]int {1, 2, 3}
    fmt.Println(a) //[1 2 3 0 0]
}

只初始化了部分元素,剩余的仍是零值。

如果我们在声明数组时同时初始化了,可以使用...而不指定数组的长度,Go会自动计算数组长度:

var a = [...]int {1, 2, 3} //初始化,数组长度为3

1.3. 短变量方式声明

当然,我们可以使用短变量声明的方式声明数组。注意:使用该方式就必须在声明的时候同时初始化

如果你只是想使用这种方式来声明一个数组,但并不初始化,可以这样做,但是必须带上{}

package main

import "fmt"

func main() {
	a := [5]int {1, 2, 3} //初始化
    b := [3]int {}
    c := [...]int {1, 2, 3}

	fmt.Println(a) //[1 2 3 0 0]
   	fmt.Println(b) //[0 0 0]
    fmt.Println(c) //[1 2 3]
}

1.4. 特殊之处

注意:在Go语言中,数组的长度是其类型的一部分。 所以Go中的数组不能改变长度。

怎么理解?下面声明了两个数组:

var a [4]int //将变量a声明为拥有4个整数的数组

var b [5]int //将变量b声明为拥有5个整数的数组

变量ab 的类型分别为[4]int[5]int,是不同的类型。

1.5. 二维数组

二维数组当中的元素仍是数组:

var ab = [2][4]int {[4]int {1, 2, 3, 4}, [4]int {4, 5, 6, 7}}
或
ab := [2][4]int {[4]int {1, 2, 3, 4}, [4]int {4, 5, 6, 7}}

可以省去数组元素的类型:

var ab = [2][4]int {{1, 2, 3, 4}, {4, 5, 6, 7}}
或
ab := [2][4]int {{1, 2, 3, 4}, {4, 5, 6, 7}}

1.6. 遍历数组

(一)使用数组长度

可以使用len(slice)函数获取数组长度,然后遍历。

arr := [5]string {"a", "b", "c", "d", "e"}

bc := [2][4]int {
    {1, 2, 3, 4}, 
    {5, 6, 7, 8},
}

for i := 0; i < len(arr); i++ {//遍历一维数组
    fmt.Println(arr[i])
}

for i := 0; i < len(bc); i++ {//遍历二维数组的元素
    fmt.Println(bc[i])
}

for i := 0; i < len(bc); i++ {//遍历二维数组的元素的元素
    for j := 0; j < len(bc[0]); j++ {
        fmt.Println(bc[i][j])
    }
}

(二)使用range关键字

range关键字用于for循环中遍历数组时,每次迭代都会返回两个值,第一个值为当前元素的下标,第二值为该下标所对应的元素值。如果这两个值的其中一个你不需要,只需使用下划线_代替即可。

arr := [5]string {"a", "b", "c", "d", "e"}

bc := [2][4]int {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
}

for i, v := range arr {//遍历一维数组
    fmt.Println(i, v)
}

for i := range arr {//遍历时只获取下标
    fmt.Println(i)
}

for _, v := range arr{//遍历时只获取元素值
    fmt.Println(v)
}

for _, v := range bc {//遍历二维数组
    for _, w := range v{
        fmt.Println(w)
    }
}

2. 切片(slice)

前面提到:Go中的数组的长度是固定的。这样就会在实际应用中带来不方便,因为很多时候在声明数组前并不明确该数组要存储多少个元素。声明太多,浪费;声明太少,不够用。

而切片就为我们提供了“动态数组”。

2.1. 使用

声明切片和声明数组类似,但是不指定长度

var sli_name []type

比如,声明一个int类型、名为a的切片:

var a []int

可以在声明它的时候直接初始化:

var a = []int {1, 2, 3, 4}

当然,也可以使用短变量的方式声明:

a := []int {1, 2, 3, 4}

可以从一个已有的数组或者已有的切片中获取切片。

获取切片的方式是通过两个下标来获取,即开始下标(startIndex)和结束下标(endIndex),二者以冒号分隔。包括startIndex,不包括endIndex

a[startIndex : endIndex]

技术图片

下面是一个例子:

a := [5]string {"a", "b", "c", "d", "e"} //数组
b := []int {1, 2, 3, 4} //切片

sliA := a[2:4]
sliB := b[1:3]

fmt.Println(sliA) //[c d]
fmt.Println(sliB) //[2 3]

2.2. 切片与数组

前面提到:切片为我们提供了“动态数组”。但该“动态数组”并不是真正意义上的能扩展长度的动态数组。

切片并不存储任何数据,它只是一个引用类型,切片总是指向一个底层的数组,描述这个底层数组的一段。

所以我们在声明数组时需要指定长度,而声明切片时不需要:

var arr = [4]int {1, 2, 3, 4} //声明数组

var slice = []int {1, 2, 3, 4} //声明切片

由于切片的底层引用的是数组,所以更改切片中的元素会修改其底层数组中对应的元素,如果还有其他切片也引用了该底层数组,那么这些切片也能观测到这些修改。如图:

技术图片

下面是一个例子:

package main

import "fmt"

func main() {
	array := [5]string {"aa", "bb", "cc", "dd", "ee"} //数组
	fmt.Println(array) //[aa bb cc dd ee]

	slice1 := array[0:2] //切片1
	slice2 := array[1:3] //切片2
	slice3 := array[2:5] //切片3

	fmt.Println(slice1) //[aa bb]
	fmt.Println(slice2) //[bb cc]
	fmt.Println(slice3) //[cc dd ee]

	slice1[0] = "xx" //修改切片1中的值
	slice2[1] = "yy" //修改切片2中的值
	slice3[2] = "zz" ////修改切片3中的值

	fmt.Println(array) //[xx bb yy dd zz]
	fmt.Println(slice1) //[xx bb]
	fmt.Println(slice2) //[bb yy]
	fmt.Println(slice3) //[yy dd zz]
}

2.3. 切片的相关操作

(一)长度

切片的长度指切片所包含的元素个数。通过函数len(s)获取切片s的长度。

(二)容量

切片的容量指切片的第一个元素到其底层数组的最后一个元素的个数。通过函数cap(s)获取切片s的容量。

下面是一个例子:

package main

import "fmt"

func main() {
	arr := [10]string {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}
	s := arr[2:5] //创建切片s

	fmt.Println(arr) //[a b c d e f g h i j]
	fmt.Println(s) //[c d e]

	fmt.Println(len(s)) //3
	fmt.Println(cap(s)) //8
}

下面是长度和容量的示意图:

技术图片

有了容量这个概念,我们就可以通过重新切片来改变切片的长度:

package main

import "fmt"

func main() {
	arr := [10]string {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}
	s := arr[2:5]
	fmt.Printf("s的长度为%d,s的容量为%d
", len(s), cap(s))
	s = s[2:8]
	fmt.Printf("s的长度为%d,s的容量为%d
", len(s), cap(s))
	s = s[0:2]
	fmt.Printf("s的长度为%d,s的容量为%d
", len(s), cap(s))
}

(三)追加元素

使用func append(slice []Type, elems ...Type) []Type可以向切片slice的末尾追加类型为Type的元素elems

该函数的结果是一个包含原切片所有元素加上新添加元素的切片。由于改变切片内容了,所以底层数组也会被改变。

package main

import "fmt"

func main() {
	s := []string {"a", "b", "c", "d"}
	s = append(s, "e") //追加1个
	s = append(s, "f", "g", "h") //追加3个
	fmt.Println(s)
}

当切片中容量已经用完时(len(s) == cap(s)),也即底层数组容纳不了追加的元素时,Go会分配一个更大的底层数组,返回的切片指向这个新分配的数组,原数组的内容不变。

package main

import "fmt"

func main() {
	arr := [5]string {"a", "b", "c", "d", "e"}
	slice1 := arr[0:2]
	fmt.Println(slice1) //[a b]
	//追加3个元素,slice1的容量已满
	slice1 = append(slice1, "1", "2", "3")
	fmt.Println(slice1) //[a b 1 2 3]
    //底层数组跟着改变
	fmt.Println(arr) //[a b 1 2 3]
	//继续追加
	slice1 = append(slice1, "4", "5")
	//指向新的底层数组
	fmt.Println(slice1) //[a b 1 2 3 4 5]
	//原底层数组不变
	fmt.Println(arr) //[a b 1 2 3]
}

(四)复制切片

func copy(dst []Type, src []Type) int

dst是目标切片,src是源切片,该函数会将src中的元素复制到dst中,并返回复制的元素个数(该返回值是两个切片长度中的小值)

package main

import "fmt"

func main() {
	slice1 := []string {"a", "b"}
	slice2 := []string {"1", "2", "3"}
	length := copy(slice2, slice1)
	//length := copy(slice1, slice2)
	fmt.Println(length)
	fmt.Println(slice1)
	fmt.Println(slice2)
}

(五)切片的默认行为

切片的默认开始下标是0,默认结束下标是切片的长度。

对于数组:

var a [10]int

下面几个切片是等价的:

a[0:10]
a[:10]
a[0:]
a[:]

2.4. 特殊切片

(一)nil切片

切片的零值是 nil,当声明一个切片,但不出初始化它,该切片便为nil切片。nil切片的长度和容量为0且没有底层数组。

func main() {
	var s []int
	fmt.Println(s, len(s), cap(s))
	if s == nil {
		fmt.Println("s切片是nil切片")
	}
}

(二)切片的切片

切片中的元素可以是切片

package main

import "fmt"

func main() {
	ss := [][]int {
		[]int {1, 2, 3}, //切片元素的类型可以省去
		[]int {4, 5, 6},
		[]int {7, 8, 9},
	}

	for i := 0; i < len(ss); i++ {
		fmt.Println(ss[i])
	}
}

2.5. 使用make函数创建切片

使用make函数可以在创建切片时指定长度和容量。make函数会分配一个元素为零值的数组并返回一个引用了它的切片

该函数接受三个参数,分别用来指定切片的类型、长度、容量。当不传入容量参数时,容量默认和长度相同。容量参数不能小于长度参数。

package main

import "fmt"

func main() {
	a := make([]int, 5)
	fmt.Println(a, len(a), cap(a)) //[0 0 0 0 0] 5 5

	b := make([]int, 5, 6)
	fmt.Println(b, len(b), cap(b)) //[0 0 0 0 0] 5 6

	//c := make([]int, 5, 4) 
	//fmt.Println(c, len(c), cap(c))//报错:len larger than cap in make([]int)
}

2.6. 遍历切片

因为切片是对数组的引用,所以遍历切片也就是在遍历数组。

3. 关于我

以上是关于Go语言系列之数组和切片的主要内容,如果未能解决你的问题,请参考以下文章

Go语言切片

从零开始学Go之容器:切片

Go 语言泛型编程之切片

通学go语言系列之容器

Go语言系列

Go语言之GO 语言引用类型