5 Go语言的内置容器

5.1 数组

数组的概念

数组是相同类型的一组数据构成的长度固定的序列,其中数据类型包含了基本数据类型、复合数据类型和自定义类型。数组中的每一项被称为数组的元素。数组名是数组的唯一标识符,数组的每一个元素都是没有名字的,只能通过索引下标(位置)进行访问。

因为数组的内存是一段连续的存储区域,所以数组的检索速度是非常快的;

但是数组也有一定的缺陷,就是定义后长度不能更改。

数组的语法

Go语言数组声明需要指定元素类型及元素个数,语法格式如下。

var 变量名 [数组长度] 数据类型

以上为一维数组的定义方式,数组长度必须是整数且大于0,未初始化的数组不是nil,也就是说没有空数组(与切片不同)。

初始化数组语法格式如下。

var nums = [5]{1,2,3,4,5}

初始化数组中 {} 中的元素个数不能大于 [] 中的数字。

如果忽略 [] 中的数字,不设置数组长度,Go语言会根据元素的个数来设置数组的长度。可以忽略声明中数组的长度并将其替换为“…”。编译器会自动计算长度。语法格式如下。

var nums = [...]int{1,2,3,4,5,6}

修改数组内容,语法格式如下:

nums[4] = 23

以上实例读取数组第5个元素。数组元素可以通过索引(位置)来读取(或者修改),索引从0开始,第1个元素索引为0,第2个索引为1,以此类推。

数组的长度

数组的长度是数组的一个内置常量,通过将数组作为参数传递给 len()函数,可以获得数组的长度。忽略声明中数组的长度并将其替换为“…”,编译器可以找到长度。接下来使用案例演示获取数组长度的方式:

package main
import "fmt"
func main(){
    a := [4]int64{12,22,33,55}
    b := [...]float64{23.3,22,23.123,12.123}
    fmt.Printf("len_a:%d,len_b:%d \n",len(a),len(b)) //len_a:4,len_b:4 
}

遍历数组

package main

import "fmt"

func main(){
    a := [4]int64{12,22,33,55}
    //b := [...]float64{23.3,22,23.123,12.123}
    // 遍历方式1
    for i:= 0;i<len(a);i++{
        fmt.Printf("%d ",a[i])
    }
    fmt.Println()
    // 遍历方式2
    for _, value := range a{
        fmt.Printf("%d ",value)
    }
}

多维数组——主要是二维数组

由于数据的复杂程度不一样,数组可能有多个下标。一般将数组元素下标的个数称为维数,根据维数,可将数组分为一维数组、二维数组、三维数组、四维数组等。二维及以上的数组可称为多维数组。

Go语言的多维数组声明方式:

var variable_name [SIZE1][SIZE2]...[SIZEn] variable_type
二维数组

在实际的工作中,仅仅使用一维数组是远远不够的,例如,一个学习小组有10个人,每个人有3门课的考试成绩,如果使用一维数组解决是很麻烦的。这时,可以使用二维数组。

二维数组是最简单的多维数组,二维数组的本质也是一个一维数组,只是数组成员由基本数据类型变成了构造数据类型(一维数组)。

二维数组的定义方式如下。

var arrayName [x][y] variable_type

二维数组初始化,语法格式如下:

a = [3][4]int{
  {0,1,2,3}, // 第一行索引为0
  {4,5,6,7}, // 第二行索引为1
  {8,9,10,11} // 第三行索引为2
}

上述定义的二维数组共包含3×4个元素,即12个元素。接下来,我们通过一张图来描述二维数组a的元素分布情况,如图5.3所示。

image-20201024182415812

二维数组元素通过指定坐标来访问,如数组中的行索引与列索引。语法格式如下。

int val = a[2][3]

以上实例访问了二维数组val第3行的第4个元素。

二维数组可以使用循环嵌套来输出元素,具体语法通过案例演示

image-20201024182517611

结果如下:

image-20201024182529965

数组是值类型

Go语言中的数组并非引用类型,而是值类型。

当它们被分配给一个新变量时,会将原始数组复制出一份分配给新变量。

因此对新变量进行更改,原始数组不会有反应。

package main
import "fmt"
func main(){
    a := [...]string{"USA","China","India","Germany"}
    b := a // 将a赋值给b
    b[0] = "Singapore"
    fmt.Println("a>>>",a) // [USA China India Germany]
    fmt.Println("b>>>",b) // [Singapore China India Germany]
}

注意:将数组作为函数参数进行传递,它们将通过值传递,原始数组依然保持不变。

5.2 切片***

切片的概念

Go语言中数组的长度不可改变,但在很多应用场景中,在初始定义数组时,数组的长度并不可预知,这样的序列集合无法满足要求。Go中提供了另外一种内置类型“切片(slice)”,弥补了数组的缺陷。切片是可变长度的序列,序列中每个元素都是相同的类型。切片的语法和数组很像。

从底层来看,切片引用了数组的对象。切片可以追加元素,在追加时可能使切片的容量增大。与数组相比,切片不需要设定长度,在[]中不用设定值,相对来说比较自由。

切片的数据结构可理解为一个结构体,这个结构体包含了三个元素。

  • 指针,指向数组中切片指定的开始位置。
  • 长度,即切片的长度。
  • 容量,也就是切片开始位置到数组的最后位置的长度。

切片的语法

1. 声明切片

声明一个未指定长度的数组来定义切片,具体示例如下:

var indentifier []type

切片不需要说明长度。采用该声明方式且未初始化的切片为空切片。该切片默认为nil,长度为0。

使用make()函数来创建切片,语法格式如下:

var slice1 []type make([]type,len)

使用make()函数来创建切片可以简写为如下格式:

slice1 := make([]type,len)

创建切片时可以指定容量,其中capacity为可选参数:make([]T, length,capacity)。详情如下:

package main
import "fmt"
func main(){
    var nums = make([]int,3,5)
    fmt.Printf("%T\n",nums)//[]int
    fmt.Printf("len=%d cap=%d slice=%v\n",len(nums),cap(nums),nums)
    //len=3 cap=5 slice=[0 0 0]
}
2. 初始化

(1)直接初始化切片,语法格式如下:

s := [] int {1,2,3}

(2)通过数组截取来初始化切片,语法格式如下:

arr := [5]int {1,2,3,4,5}
s := arr[:]

切片中包含数组所有元素,语法格式如下。

s := arr[startIndex : endIndex]

将arr中从下标startIndex到endIndex-1下的元素创建为一个新的切片(前闭后开),长度为endIndex-startIndex。

缺省endIndex时表示一直到arr的最后一个元素,语法格式如下。

s := arr[startIndex:]

缺省startIndex时表示从arr的第一个元素开始,语法格式如下。

s := arr[:endIndex]

(3)通过切片截取来初始化切片。

可以通过设置下限及上限来设置截取切片:[lower-bound:upper-bound]:

package main
import "fmt"
func main(){
    // 创建切片
    nums := []int{0,1,2,3,4,5,6,7,8,9}
    printSlice(nums) //len=10,cap=10,slice=[0 1 2 3 4 5 6 7 8 9]

    // 从索引1(包含)到4(不包含)
    fmt.Println("1->4:::",nums[1:4]) //1->4::: [1 2 3]

    // 使用切片初始化
    nums1 := nums[1:4]
    printSlice(nums1)//len=3,cap=9,slice=[1 2 3]
}

func printSlice(x []int){
    fmt.Printf("len=%d,cap=%d,slice=%v\n",len(x),cap(x),x)
}

Len()和cap()函数

切片的长度是切片中元素的数量。切片的容量是从创建切片的索引开始的底层数组中元素的数量。

切片可以通过len()方法获取长度,可以通过cap()方法获取容量。数组计算cap()结果与len()相同。

切片是引用类型 ***

切片没有自己的任何数据。它只是底层数组的一个引用。对切片所做的任何修改都将反映在底层数组中。数组是值类型,而切片是引用类型,两者的区别如下例所示:

package main

import "fmt"

func main(){
    a := [4]float64{56.1,22.2,33.12,55.45}
    b := []int{2,3,5}
    fmt.Printf("变量a —— 地址:%p,类型:%T,数值:%v,长度:%d\n",&a,a,a,len(a))
    fmt.Printf("变量b —— 地址:%p,类型:%T,数值:%v,长度:%d\n",&b,b,b,len(b))
    c := a
    d := b
    fmt.Printf("变量c —— 地址:%p,类型:%T,数值:%v,长度:%d\n",&c,c,c,len(c))
    fmt.Printf("变量d —— 地址:%p,类型:%T,数值:%v,长度:%d\n",&d,d,d,len(d))

    c[1] = 666
    fmt.Println("a=",a,"c=",c) // a= [56.1 22.2 33.12 55.45] c= [56.1 666 33.12 55.45]

    // 因为切片是引用类型(跟Python一样!)所以改变d的话同时会影响b中的数据!!!
    d[1] = 999
    fmt.Println("b=",b,"d=",d) // b= [2 999 5] d= [2 999 5]
}

append()和copy()函数

两个方法不适用于数组。

函数append()用于往切片中追加新元素,可以向切片里面追加一个或者多个元素,也可以追加一个切片。append()会改变切片所引用的数组的内容,从而影响到引用同一数组的其他切片。当使用append()追加元素到切片时,如果容量不够(也就是(cap-len) == 0),Go就会创建一个新的内存地址来储存元素。

函数copy()会复制切片元素,将源切片中的元素复制到目标切片中,返回复制的元素的个数。 copy()方法不会建立源切片与目标切片之间的联系。也就是两个切片不存在联系,其中一个修改不影响另一个。

删除第一个或最后一个元素的方法:

image-20201024190435588

删除中间元素的方法:

image-20201024190457761

append()与copy()的对比案例一***

package main

import "fmt"

// 输出切片格式化信息
func printSlices(name string,x []int){
    fmt.Print(name,"\t")
    fmt.Printf("addr:%p len=%d cap=%d slice=%v \n",x,len(x),cap(x),x)
}

func main(){
    fmt.Println("1. ----------------------------")
    numbers := make([]int, 0, 20)
    printSlices("numbers",numbers) //numbers addr:0xc00006e000 len=0 cap=20 slice=[]
    //append
    numbers = append(numbers,0,1)
    printSlices("numbers",numbers) //numbers addr:0xc00006e000 len=2 cap=20 slice=[0 1]
    fmt.Println("2. ----------------------------")
    //追加一个切片
    s1 := []int{100,200,300,400,500}
    numbers = append(numbers,s1...)
    printSlices("numbers",numbers) // numbers addr:0xc000134000 len=7 cap=20 slice=[0 1 100 200 300 400 500]
    fmt.Println("3. ----------------------------")
    // 切片删除元素
    // 删除第一个元素
    numbers = numbers[1:]
    // 删除最后一个元素
    numbers = numbers[:len(numbers)-1]
    // 删除中间元素
    a := int(len(numbers)/2) // 中间数
    numbers = append(numbers[:a],numbers[a+1:]...)
    fmt.Println("4. ----------------------------")

    // 创建切片 mumbers1 是之前切片的两倍容量
    numbers1 := make([]int,len(numbers),(cap(numbers))*2)
    // 复制numbers的内容到numbers1 —— count是复制的个数
    count := copy(numbers1,numbers)
    fmt.Println("复制的个数:",count) // 4
    printSlices("numbers1",numbers1)//numbers1   addr:0xc000070000 len=4 cap=38 slice=[1 100 300 400]

    /*
    numbers1与numbers两者不存在联系,更改numbers的值不会影响numbers1!
    说明copy方法是不会建立两个切片的联系的!
    */
    numbers[0] = 666
    numbers1[0] = 888
    printSlices("numbers",numbers)//numbers addr:0xc000134008 len=4 cap=19 slice=[666 100 300 400]
    printSlices("numbers1",numbers1)//numbers1   addr:0xc000136000 len=4 cap=38 slice=[888 100 300 400]
}

append()与copy()的对比案例二***

package main

import (
    "fmt"
    "strconv"
)

// 输出切片格式化信息
func printSlices(name string,x []string){
    fmt.Print(name," ")
    fmt.Printf("addr:%p len=%d cap=%d slice=%v \n",x,len(x),cap(x),x)
}

func main(){
    // 思考:使用那种初始化切片的方式更高效?
    var sa []string
    //sa := make([]string,0,10)
    printSlices("sa",sa)

    // 当使用append追加元素到切片时,如果容量不够,Go会创建一个新的切片变量来存储元素
    for i := 0; i < 10; i++{
        sa = append(sa,strconv.Itoa(i))
        printSlices("sa",sa)
    }
    printSlices("sa",sa)
}
// 可以看到容量cap的值在增加!
/*
sa addr:0x0 len=0 cap=0 slice=[]
sa addr:0xc00008e1f0 len=1 cap=1 slice=[0]
sa addr:0xc0000a6060 len=2 cap=2 slice=[0 1]
sa addr:0xc0000b2040 len=3 cap=4 slice=[0 1 2]
sa addr:0xc0000b2040 len=4 cap=4 slice=[0 1 2 3]
sa addr:0xc0000bc000 len=5 cap=8 slice=[0 1 2 3 4]
sa addr:0xc0000bc000 len=6 cap=8 slice=[0 1 2 3 4 5]
sa addr:0xc0000bc000 len=7 cap=8 slice=[0 1 2 3 4 5 6]
sa addr:0xc0000bc000 len=8 cap=8 slice=[0 1 2 3 4 5 6 7]
sa addr:0xc0000be000 len=9 cap=16 slice=[0 1 2 3 4 5 6 7 8]
sa addr:0xc0000be000 len=10 cap=16 slice=[0 1 2 3 4 5 6 7 8 9]
sa addr:0xc0000be000 len=10 cap=16 slice=[0 1 2 3 4 5 6 7 8 9]
*/

5.3 map

map的概念

Go 语言提供了内置类型 map,它将一个值与一个键关联起来,可以使用相应的键检索值。这种结构在其他资料中译成地图、映射或字典,但是在Go语言中习惯上翻译成集合。map正如现实生活中的字典一样,使用词-语义进行数据的构建,其中词对应键(key),语义对应值(value),即键与值构成映射的关系,通常将两者称为键值对,这样通过键可以快速找到对应的值。map是一种集合,可以像遍历数组或切片那样去遍历它。因为map是由Hash表实现的,所以对map的读取顺序不固定。

map是无序的,每次打印出来的map都会不一样,它不能通过index获取,而必须通过key获取。

map的长度是不固定的,和切片一样可以扩展。内置的len()函数同样适用于map,返回map拥有的键值对的数量。

但是map不能通过cap()函数计算容量(或者说cap()函数的参数不可以是map)。

同一个map中key必须保证唯一。key的数据类型必须是可参与比较运算的类型,也就是支持==或!=操作的类型,如布尔型、整型、浮点型、字符串、数组。切片、函数等引用类型则不能作为key的数据类型

map的value可以是任何数据类型。map和切片一样,也是一种引用类型。

map的语法 ***

map的声明

可以使用var关键字来定义map,也可以使用内建函数make()。

(1)使用var关键字定义map

var 变量名 map[key类型] value类型

使用var关键字声明map,未初始化的map的默认值是nil。

nil map不能存放键值对。如果要使用map存储键值对,必须在声明时初始化,或者使用make()函数分配到内存空间。

(2)使用make()函数

变量名 := make(map[key类型]value类型)

该声明方式,如果不初始化map,map也不等于nil。

map的初始化赋值和遍历

package main

import (
    "fmt"
)

func main(){
    // 1. 声明时同时初始化
    var country = map[string]string{
        "China":"Beijing",
        "Japan":"Tokyo",
        "USA":"DC",
    }
    fmt.Println(country) // map[China:Beijing Japan:Tokyo USA:DC]

    // 短变量声明初始化方式
    fating := map[string]float64{
        "a":12.23,
        "b":22.24,
        "c":34.21,
    }
    fmt.Println(fating) // map[a:12.23 b:22.24 c:34.21]

    // 2. 创建map后再赋值
    countryMap := make(map[string]string)
    countryMap["China"] = "Beijing"
    countryMap["Japan"] = "Tokyo"
    countryMap["France"] = "paris"
    fmt.Println(countryMap) // map[China:Beijing France:paris Japan:Tokyo]

    // 3. 遍历map(无序)
    // (1)key、value都遍历
    for k, v := range countryMap{
        fmt.Println("国家:",k," 首都:",v)
    }
    /*
    国家: China  首都: Beijing
    国家: Japan  首都: Tokyo
    国家: France  首都: paris
    */
    // (2) 只展示key
    for k := range countryMap{
        fmt.Println(k)
    }
    // (3)只展示value
    for _, value := range countryMap{
        fmt.Println(value)
    }
}

查看元素是否在map中 ***

可以通过key获取map中对应的value值。语法为:map[key]。当key不存在时,会得到该value值类型的默认值,比如string类型得到空字符串,int类型得到0,程序不会报错。

所以可以通过value, ok := map[key]获知key/value是否存在。ok是bool型,如果ok是true,则该键值对存在,否则不存在。

package main

import (
    "fmt"
)

func main(){

    var country = map[string]string{
        "China":"Beijing",
        "Japan":"Tokyo",
        "USA":"DC",
    }
    fmt.Println(country) // map[China:Beijing Japan:Tokyo USA:DC]
    // 查看元素是否在map中
    query_str := "China"
    value, ok := country[query_str]
    fmt.Printf("%q\n",value) // ""
    fmt.Printf("%T , %v\n",ok,ok) //bool , false
    if ok{
        fmt.Println("找到了!")
    }else{
        fmt.Println("没找到!")
    }

    // 或者
    if value, ok := country[query_str];ok{
        fmt.Println("找到了!,首都:",value)
    }else{
        fmt.Println("没得!")
    }
}

delete()函数

delete(map, key) 函数用于删除集合的某个元素,参数为map和其对应的key。删除函数不返回任何值。

package main

import (
    "fmt"
)

func main(){

    var country = map[string]string{
        "China":"Beijing",
        "Japan":"Tokyo",
        "USA":"DC",
    }
    fmt.Println(country) // map[China:Beijing Japan:Tokyo USA:DC]

    // 根据key删除map中的某个元素
    fmt.Println("删除前:",country) //删除前: map[China:Beijing Japan:Tokyo USA:DC]
    if _,ok := country["Japan"];ok{
        delete(country,"China")
    }
    fmt.Println("删除后:",country) //删除后: map[Japan:Tokyo USA:DC]

    // 清空map
    country = make(map[string]string)
    fmt.Println("清空后:",country) //清空后: map[]
}

Go语言没有为map提供清空所有元素的函数,清空map的唯一办法是重新make一个新的map。不用担心垃圾回收的效率,Go语言的垃圾回收比写一个清空函数更高效。

map是引用类型

map与切片相似,都是引用类型。

将一个map赋值给一个新的变量时,它们指向同一块内存(底层数据结构)。因此,修改两个变量的内容都能够引起它们所指向的数据发生变化。

package main

import (
    "fmt"
)

func main(){

    var country = map[string]string{
        "China":"Beijing",
        "Japan":"Tokyo",
        "USA":"DC",
    }
    fmt.Println(country) // map[China:Beijing Japan:Tokyo USA:DC]

    newCountry := country
    newCountry["USA"]  = "CCCCCC"
    fmt.Println(country) // map[China:Beijing Japan:Tokyo USA:CCCCCC]
    fmt.Println(newCountry) // map[China:Beijing Japan:Tokyo USA:CCCCCC]
}