数组

与大多数语言中的数组都类似,Go语言的数组也是从声明时就确定,使用时可以修改数组成员,但是数组大小不可变化。但是实际上,Go中的数组并不常用,而常用为切片。

声明

1
var 数组变量名 [元素数量]T

初始化

基础写法

1
2
3
var testArray [3]int                        	//数组会初始化为int类型的零值
var numArray = [3]int{1, 2} //使用指定的初始值完成初始化
var cityArray = [3]string{"北京", "上海", "深圳"} //使用指定的初始值完成初始化

编译器判断长度

1
2
var numArray = [...]int{1, 2}
var cityArray = [...]string{"北京", "上海", "深圳"}

指定索引值

1
r := [...]int{99: -1}

定义了一个含有 100 个元素的数组 r,最后一个元素被初始化为 -1,其它元素都是用 0 初始化

数组遍历

与大多数语言一样,Go语言遍历的方式主要分为两种方法:for和for range

1
2
3
4
5
6
7
8
9
// 方法1:for循环遍历
for i := 0; i < len(a); i++ {
fmt.Println(a[i])
}

// 方法2:for range遍历
for index, value := range a {
fmt.Println(index, value)
}

数组是值类型

Go语言的数组是值类型,并不是引用类型。这导致Go语言与C、Java不同的是,Go的数组赋值和传参会复制整个数组。因此传参或是赋值,得到的新数组是原数组的一个副本,修改新数组并不会导致原数组发生变化。

1
2
3
4
5
6
7
8
9
func changeArray(x [3]int) {
x[0] = 100
}

func main() {
a := [3]int{10, 20, 30}
changeArray(a) //在changeArray中修改的是a的副本x
fmt.Println(a) //[10 20 30],原数组无变化
}

注意点

  1. [n]*T表示指针数组,*[n]T表示数组指针
  2. 在Go语言中,数组的长度也是类型的一部分,也就意味着在函数的形参处,数组长度必须要确定,这导致了输入数组的长度必须是相同的,使得数组有很多局限性

切片

前面我们提到,数组在函数形参初输入数组长度必须是相同,这导致数组有很多局限性

Go语言提供了一种非常灵活的,拥有相同类型元素的可变长度的序列——切片(Slice)。切片的概念类似于“动态数组”,它是基于数组类型做的一层封装,支持自动扩容。

切片是一个引用类型,因此从数组中得到的切片修改元素值时,原数组也会发生变化,修改原数组时,切片也会变化。切片一般用于快速地操作一块数据集合。

它的内部结构包括地址长度容量

切片的定义

长度与容量

正如上文所言,切片拥有长度和容量,这与其他语言中的数组类似。

我们可以通过内置的len()函数求得长度,通过内置的cap()函数求得容量。

声明的基本语法

1
var 变量名 []类型

切片表达式构造切片

切片表达式可以从字符串、数组、指向数组或切片的指针构造子字符串或切片。

它具有两种形式:

  1. 指定low和high两个索引界限值的简单形式——low:high
  2. 除了low和high索引界限值外还指定容量的完整形式——low:high:max

简单形式

我们通过表达式中的low和high就可以确定数组中的索引范围,为一个左包含,右不包含。得到的切片长度为high-low,容量等于切片的底层数组的容量

1
2
3
4
a := [5]int{1, 2, 3, 4, 5}
s := a[1:3]
fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s))
//s:[2 3] len(s):2 cap(s):4

在切片表达式中,我们可以省略任何索引:

  • 省略low为从0开始
  • 省略high为到切片操作数的长度

注意:对于切片再切片,high上线边界为原切片的容量cap(a),而不是长度。

完整形式

对于数组,指向数组的指针,或切片(不支持字符串)支持完整切片表达式

1
a[low:high:max]

该表达式会构建一个与a[low:high]同类型、同长度、同元素,但是容量会被设置为max-low的切片。

在完整切片表达式中,只有第一个索引值low可以省略。

1
2
3
4
a := [5]int{1, 2, 3, 4, 5}
t := a[1:3:5]
fmt.Printf("t:%v len(t):%v cap(t):%v\n", t, len(t), cap(t))
//t:[2 3] len(t):2 cap(t):4

make()函数构造切片

切片表达式都是基于现有数组来创建的切片,如果我们需要动态的创建一个切片,则我们需要用到内置的make()函数

1
make([]T, size, cap)

其中,T为切片的元素类型,size为元素长度,cap为容量

可以只指定长度,也开也长度容量都指定

1
2
3
s1 := make([]uint32)
s2 := make([]uint32, 1)
s3 := make([]uint32, 1, 10)

切片的性质

判断切片是否为空

要检查切片是否为空,请使用len(s) == 0来判断,而不应该使用s == nil来判断。

没有底层数组的切片值为nil,一个nil切片长度和容量都为0,但是长度和容量都为0的切片不一定是nil:

1
2
3
var s1 []int         //len(s1)=0;cap(s1)=0;s1==nil
s2 := []int{} //len(s2)=0;cap(s2)=0;s2!=nil
s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil

因此,要判断一个切片是否为空,要使用len(s) == 0来判断,而不应该使用s == nil来判断

切片不能直接相互比较

切片之间是不能直接相互比较的,我们不能使用==判断两个切片是否含有相同的元素。

切片唯一合法的比较操作是和nil比较。若要比较,则只能通过枚举比较的方式进行比较。

对于[]byte,可以使用标准库提供的bytes.Equal函数比较。

切片的本质

切片的本质是对底层数组的封装。它包含了三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)。

例子:数组a := [8]int{0, 1, 2, 3, 4, 5, 6, 7},切片s1 := a[:5],相应示意图如下:

slice_01

切片s2 := a[3:6],相应示意图如下:

slice_02

CRUD

添加元素

通过append()函数可以向切片的底层数组中追加元素,可以添加0个或多个元素,并且切片的底层数组会自动扩容。

每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()函数调用时,所以我们通常都需要用原变量接收append函数的返回值

1
2
3
4
5
6
7
s := make([]uint32, 0, 4)

s = append(s, 1, 2, 3)
fmt.Println(len(s), cap(s)) // 3 4

s = append(s, 4, 5, 6)
fmt.Println(len(s), cap(s)) // 6 8

如果需要追加其他切片或数组,可以在需添加的切片后面...

1
s = append(s, s1...)

切片遍历

切片遍历与数组遍历是相同的,支持:

  • 索引遍历for
  • for index, value := range s

修改元素

通过下标指定即可修改。

注意:在使用for range时,不能通过直接修改遍历所使用的变量,因为range赋值得到的变量是一份副本,并不是引用。例如:for k,v:= range s,只能通过修改s[k],而不能直接修改v。

删除元素

截取需要留下部分的,在赋值给原切片即可。

1
2
var s = []int{1, 2, 3, 4}
s = append(s[:2], s[3:]...)

切片拷贝

引用拷贝

直接使用=操作符赋值的拷贝,新切片得到的是引用拷贝,原切片与新切片所指向的底层数组为同一个数组

1
2
3
4
5
s1 := make([]int, 3) //[0 0 0]
s2 := s1 //将s1直接赋值给s2,s1和s2共用一个底层数组
s2[0] = 100
fmt.Println(s1) //[100 0 0]
fmt.Println(s2) //[100 0 0]

值拷贝

如果我们需要拷贝一个值相同的切片,则我们需要用到copy()函数,该函数可以将原数组切片复制到新的数组切片。

1
copy(newSlice, oldSlice)

如果两个数组切片不一样大,则会按照较小的一个数组切片的元素个数进行赋值。

1
2
3
4
5
s1 := []int{1, 2, 3, 4, 5} 
s2 := []int{5, 4, 3}

copy(s2, s1) // 只会复制s1的前3个元素到s2中
copy(s1, s2) // 只会复制s2的3个元素到s1的前3个位置