数组

数组是同一种数据类型元素的集合。在Go语言中,数组从声明时就确定,可以对其成员进行修改,但是不可以修改数组的大小。

数组的定义

数组的基本定义语法如下:

var 数组名 [数组大小]类型

比如定义一个长度为3,类型是int的数组:

var a [3]int

注意:长度必须是常量,它是数组类型的一部分,一旦定义,长度不能改变。

数组的初始化

(1)、初始化数组时可以使用初始化列表来设置数组元素的值。

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

(2)、按照上面的方法每次都要确保提供的初始值和数组长度一致,一般情况下我们可以让编译器根据初始值的个数自行推断数组的长度,例如:

func main() {
    var testArray [3]int
    var numArray = [...]int{1, 2}
    var cityArray = [...]string{"北京", "上海", "深圳"}
    fmt.Println(testArray)                          //[0 0 0]
    fmt.Println(numArray)                           //[1 2]
    fmt.Printf("type of numArray:%T\n", numArray)   //type of numArray:[2]int
    fmt.Println(cityArray)                          //[北京 上海 深圳]
    fmt.Printf("type of cityArray:%T\n", cityArray) //type of cityArray:[3]string
}

(3)、我们还可以使用指定索引值的方式来初始化数组,例如:

func main() {
    a := [...]int{1: 1, 3: 5}
    fmt.Println(a)                  // [0 1 0 5]
    fmt.Printf("type of a:%T\n", a) //type of a:[4]int
}

数组的遍历

数组的遍历有两种方法:

  • for range
  • 通过索引遍历

如下:

package main

import "fmt"

func main() {
    var a = [...]string{"北京", "上海", "广州"}
    // 通过索引遍历数组
    for i := 0; i < len(a); i++ {
        fmt.Println(a[i])
    }
    fmt.Println("================")

    // 用for range遍历数组
    for _, v := range a {
        fmt.Println(v)
    }
}

多维数组

Go语言支持多维数组,这里以二维数组为例。

二维数组的定义

二维数组的基本定义如下:

var 数组名 [数组大小][数组大小]类型

如下表示外层数组有三个元素,里层数组有两个元素的二维数组:

var a [3][2]int

二维数组的初始化

package main

import "fmt"

func main() {
    var cities = [3][2]string{
        {"北京", "上海"},
        {"广州", "重庆"},
        {"四川", "贵州"},
    }
    fmt.Println(cities)
}

如果要用自动推导数组长度,只有第一层可以使用,如下:

//支持的写法
a := [...][2]string{
    {"北京", "上海"},
    {"广州", "深圳"},
    {"成都", "重庆"},
}
//不支持多维数组的内层使用...
b := [3][...]string{
    {"北京", "上海"},
    {"广州", "深圳"},
    {"成都", "重庆"},
}

注意:数组是值类型的,其赋值和传参会复制整个数组,因此改变副本的值,不会改变本身的值。如下:

package main

import "fmt"

func main() {
    var cities = [3][2]string{
        {"北京", "上海"},
        {"广州", "重庆"},
        {"四川", "贵州"},
    }
    // fmt.Println(cities)
    c2 := cities
    c2[2][0] = "黑龙江"
    fmt.Println(cities)
    fmt.Println(c2)
}

其输出结果为:

[[北京 上海] [广州 重庆] [四川 贵州]]
[[北京 上海] [广州 重庆] [黑龙江 贵州]]

3.3、切片

数组的长度是不可变的,一旦我们定义了某个数组,其长度就固定了。而切片是对数组的一层封装,它是拥有相同类型元素的可变长度序列。它非常灵活,支持自动扩容。

切片是一个引用类型,其底层依然是某个数组,如果底层数据改变,切片也会相应的改变。切片的内容包括地址 长度 容量 。它一般用来快速的操作一块数据集合。

自定义切片

切片的基本语法如下:

var 变量名 []元素类型

注意:在定义切片的时候,不指定其长度。

例如:

package main

import "fmt"

func main() {
    // 定义切片
    var s1 []int
    var s2 []string
    var s3 []bool
    fmt.Println(s1, s2, s3)
}

由于切片是引用类型,它不支持直接比较,只支持和nil 比较,如下:

package main

import "fmt"

func main() {
    // 定义切片
    var s1 []int
    var s2 []string
    var s3 []bool
    fmt.Println(s1, s2, s3)
    fmt.Println(s1 == nil)
    fmt.Println(s2 == nil)
    fmt.Println(s3 == nil)
    // fmt.Println(s1 == s3) // 不能这么比较
}

自定义切片的初始化

切片的初始化和数组一样,因为其底层本身就是数组。比如:

package main

import "fmt"

func main() {
    // 定义切片并初始化
    var s4 = []int{1, 2, 3, 4}
    var s5 = []string{"北京", "上海"}
    var s6 = []bool{true, false}
    fmt.Println(s4, s5, s6)
}

基于数组定义切片

我们知道切片底层是存储,所以切片也可以基于数组进行定义。如下:

package main

import "fmt"

func main() {
    // 基于数组定义切片
    // 先定义一个数组
    var a1 = [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
    // 然后对数组进行切割
    var a2 = a1[2:6]
    var a3 = a1[1:]
    var a4 = a1[:4]
    var a5 = a1[:]
    fmt.Println(a1, a2)
}

基于切片定义切片

切片还可以基于切片定义切片。如下:

package main

import "fmt"

func main() {
    // 基于切片定义切片
    // 定义一个切片
    var s = []int{1,2,3,4,5,6,7,8,9}
    // 对切片进行切割
    var s1 = s[2:4]
    fmt.Println(s1)
}

注意:对切片进行再切片的时候,其索引不能超过原数组的长度。

切片的长度和容量

切片拥有自己的长度和容量,我们可以通过使用内置的len()函数求长度,使用内置的cap()函数求切片的容量。

package main

import "fmt"

func main() {
    // 基于切片定义切片
    // 定义一个切片
    var s = []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
    // 对切片进行切割
    var s1 = s[2:4]
    fmt.Println(s1)
    fmt.Printf("len(s1):%d  cap(s1):%d", len(s1), cap(s1))
}

输出如下:

[3 4]
len(s1):2  cap(s1):7

说明:

  • len函数求出来的长度就是切片的现有长度
  • cap函数求出来的容量是从切片起始位置到数组末尾的长度

用make函数构造切片

上面介绍的都是通过数组来定义切片,不论是通过自定义还是从数组切片来的,其长度和容量都在定义的时候固定了。那么,如果我们需要动态创建一个切片,就需要使用make 函数,格式如下:

make([]T, size, cap)

说明:

  • T:表示元素的类型
  • size:表示元素的数量
  • cap:表示切片的容量

比如,我们创建一个元素个数是5,容量是10的切片。

package main

import "fmt"

func main() {
    s := make([]int, 5, 10)
    fmt.Println(s)
    fmt.Printf("len(s):%d cap(s):%d\n", len(s), cap(s))
}

输出如下:

[0 0 0 0 0]
len(s):5 cap(s):10

如果我们不指定容量,其默认和前面的元素个数一致,如下:

package main

import "fmt"

func main() {
    s := make([]int, 5)
    fmt.Println(s)
    fmt.Printf("len(s):%d cap(s):%d", len(s), cap(s))
}

输出如下:

[0 0 0 0 0]
len(s):5 cap(s):5

切片的本质

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

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

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

切片之间是不能比较的,我们不能使用==操作符来判断两个切片是否含有全部相等元素。 切片唯一合法的比较操作是和nil比较。 一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的切片一定是nil,例如下面的示例:

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来判断。

注意以下三点:

  • 切片不保存具体的值
  • 切片对应一个底层数组
  • 底层数组都是占用一块连续的内存空间

切片的赋值拷贝

我们知道切片的本质是对底层数组的封装,是一个引用类型,所以如果两个切片共用底层数组,那么对一个切片进行修改会影响另一个切片的内容,如下:

package main

import "fmt"

func main() {
    s := make([]int, 5)
    fmt.Println(s)
    fmt.Printf("len(s):%d cap(s):%d\n", len(s), cap(s))
    s1 := s
    s1[0] = 100
    fmt.Println(s)
    fmt.Println(s1)
}

其输出结果如下:

[0 0 0 0 0]
len(s):5 cap(s):5
[100 0 0 0 0]
[100 0 0 0 0]

用copy函数复制切片

上面介绍的赋值拷贝,两个切片是共用同一个底层数组,对任意一个进行修改就会影响另一个切片。Go语言中有另一个函数copy(),它是会开辟另外一个内存空间用来保存复制来的切片,通过这个函数复制的切片与原切片没有任何关系。

其基本语法如下:

copy(destSlice, srcSlice)

其中:

  • destSlice:是目标切片
  • srcSlice:是源切片

例如:

package main

import "fmt"

func main() {
    // 定义一个切片并初始化
    s1 := []int{1, 2, 3, 4}
    // 再定义一个切片
    s2 := make([]int, 4, 4)
    // 用copy()函数进行复制
    copy(s2, s1)
    // 输出:源切片s1:[1 2 3 4]  目标切片s2:[1 2 3 4]
    fmt.Printf("源切片s1:%v  目标切片s2:%v\n", s1, s2)
    // 对源切片进行修改,观察目标切片有无变化
    s1[0] = 100
    // 输出:源切片s1:[100 2 3 4]  目标切片s2:[1 2 3 4]
    fmt.Printf("源切片s1:%v  目标切片s2:%v\n", s1, s2)
    // 对目标切片进行修改,观察源切片有无变化
    s2[1] = 300
    // 输出:源切片s1:[100 2 3 4]  目标切片s2:[1 300 3 4]
    fmt.Printf("源切片s1:%v  目标切片s2:%v\n", s1, s2)
}

从上面可以看出,用copy 复制的切片,相互之前并不影响。

切片的遍历

切片的遍历和数组一样,如下:

package main

import "fmt"

func main() {
    s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
    // 第一种:遍历索引
    for i := 0; i < len(s); i++ {
        fmt.Println(s[i])
    }
    // 第二种:用for range遍历
    for i, v := range s {
        fmt.Println(i, v)
    }
}

给切片添加元素

上面介绍了切片的长度不是固定,那么我们就可以往切片里添加新的内容。在Go语言中,给切片添加内容是用append()方法。

注意:

  1. 如果切片的容量够用,则直接向切片中添加内容;
  2. 如果切片的容量不够,则会先一定的策略进行扩容,然后再向里面添加内容;

比如:

package main

import "fmt"

func main() {
    // 定义一个数组
    a := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
    // 然后从数组得到一个切片
    s := a[:4]
    // 然后打印切片的长度和容量,这时候的长度是小于容量的。
    // 输出:[1 2 3 4] len(s):4 cap(s):9
    fmt.Printf("%v len(s):%d cap(s):%d\n", s, len(s), cap(s))
    // 我们对切片s进行添加元素操作
    s = append(s, 100)
    // 输出:[1 2 3 4 100] len(s):5 cap(s):9
    fmt.Printf("%v len(s):%d cap(s):%d\n", s, len(s), cap(s))

    // 我们重新从数组得到一个切片,这次用全量的
    s2 := a[:]
    // 输出:[1 2 3 4 100 6 7 8 9] len(s2):9 cap(s2):9
    fmt.Printf("%v len(s2):%d cap(s2):%d\n", s2, len(s2), cap(s2))
    // 我们对其进行添加元素
    s2 = append(s2, 200)
    // 输出:[1 2 3 4 100 6 7 8 9 200] len(s2):10 cap(s2):18
    fmt.Printf("%v len(s2):%d cap(s2):%d\n", s2, len(s2), cap(s2))

}

上面的输出正好验证了我们所说,如果切片的容量充足,添加元素则直接再后面追加;如果切片容量不足,添加元素则先对容量进行扩充,再在后面添加元素。

当然,append() 函数还支持一次性扩容多个元素,如下:

package main

import "fmt"

func main() {
    // 定义一个数组
    a := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
    // 我们重新从数组得到一个切片,这次用全量的
    s2 := a[:]
    // 输出:[1 2 3 4 100 6 7 8 9] len(s2):9 cap(s2):9
    fmt.Printf("%v len(s2):%d cap(s2):%d\n", s2, len(s2), cap(s2))
    // 我们对其进行添加元素
    s2 = append(s2, 200)
    // 输出:[1 2 3 4 100 6 7 8 9 200] len(s2):10 cap(s2):18
    fmt.Printf("%v len(s2):%d cap(s2):%d\n", s2, len(s2), cap(s2))
    s2 = append(s2, 300, 400, 500)
    fmt.Printf("%v len(s2):%d cap(s2):%d\n", s2, len(s2), cap(s2))
}

切片的扩容策略

通过上面对切片添加元素知道当切片的容量不足的时候会先给切片扩容。扩容也是有一定策略的,其策略如下:

  1. 如果申请的容量大于原来容量的2倍,最终的容量则是申请的容量大小;
  2. 如果旧切片的容量小于1024,则最终容量是旧容量的2倍;
  3. 如果旧切片的容量大于1024,则最终容量是从旧容量开始循环增加原来的1/4,直到最终容量大于等于新申请的容量;
  4. 如果最终容量计算值溢出,则最终容量是新申请容量;

注意:切片扩容还会根据切片中元素的类型不同而做不同的处理,比如intstring类型的处理方式就不一样。

从切片中删除元素

Go语言中并没有删除切片元素的专用方法,但是我们可以使用切片本身的特性来删除元素。

比如要删除切片a中所以为index的元素,则用以下方法:

a = append(a[:index], a[index+1:]...)

例如:

package main

import "fmt"

func main() {
    // 定义一个切片
    s1 := []int{1, 2, 3, 4, 5}
    // 从切片中删除值为3的元素
    s1 = append(s1[:2], s1[3:]...)
    // 输出:[1 2 4 5]
    fmt.Println(s1)
}

我们从切片中删除某个元素对原数组有什么影响呢?我们看下面这个例子:

package main

import "fmt"

func main() {
    // 定义一个数组
    a := [...]int{1, 2, 3, 4, 5, 6}
    s2 := a[:]
    s2 = append(s2[:2], s2[3:]...)
    fmt.Println(s2) // 输出:[1 2 4 5 6]
    fmt.Println(a)  // 输出:[1 2 4 5 6 6]
}

我们可以看到从切片中删除某个元素,对原数据也有影响。如下图: