Go语言内置容器(数组(array)、切片(slice)和映射(map))

Go语言的内置容器主要有数组(array)、切片(slice)和映射(map)。

 

数组(array)

数组是具有相同类型且长度固定的一组数据项序列,这组数据项序列对应存放在内存中的一块连续区域中,数组大小在声明时指定后不可再变。

 

声明数组

格式如下:

var 数组变量名 [数组长度]元素类型
// 例:
var student [3]string
var grid [4][5]bool // 二维数组

 

初始化数组

  1. 数组可以在声明时进行赋值:

    var student [3]string{"Tom", "Ben", "Peter"}
    
  2. 可以用...代替中括号里面的数字,Go语言编译器在编译时可以根据元素的个数来设置数组的大小

    var student = [...]string{"Tom", "Ben", "Peter"}
    
  3. 指定index赋值

    var arr = [...]int{1: 800, 0: 900, 2: 999}
    fmt.Println("arr = ", arr)
    
  4. 二维数组初始化,第1维可以用...推测,第2维不能用...

    var arr = [...][3]int{{1}, {2, 3}}
    

 

遍历数组

通过for range遍历数组时,取得的是数组里每一个元素的拷贝

arr := [...]int{1, 2, 3}
for i, ele := range arr { //ele是arr中元素的拷贝
    arr[i] += 8 //修改arr里的元素,不影响ele
    fmt.Printf("%d %d %d\n", i, arr[i], ele)
    ele += 1 //修改ele不影响arr
    fmt.Printf("%d %d %d\n", i, arr[i], ele)
}
for i := 0; i < len(arr); i++ {
    fmt.Printf("%d %d\n", i, arr[i])
}

在这里插入图片描述

 

注意事项

  1. 数组的长度和元素类型都是数组类型的一部分,故[10]int[20]int是不同的类型。

  2. 在 Go 中,与 C 数组变量隐式作为指针使用不同,Go 数组是值类型,赋值和函数传参操作都会复制整个数组数据

    func testArray(x [2]int) {
    	fmt.Printf("func Array : %p , %v\n", &x, x)
    }
    
    func main() {
    	arrayA := [2]int{100, 200}
    	var arrayB [2]int
    	arrayB = arrayA
    	fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA)
    	fmt.Printf("arrayB : %p , %v\n", &arrayB, arrayB)
    	testArray(arrayA)
    }
    

在这里插入图片描述

  1. 如果两个数组类型相同的情况下,我们可以直接通过比较运算符( ==和!=)来判断两个数组是否相等,只有当两个数组的所有元素都是相等的时候数组才是相等的,不能比较两个类型不同的数组,否则程序将无法完成编译。

    a := [2]int{1, 2}
    b := [...]int{1, 2}
    c := [2]int{1, 3}
    fmt.Println(a == b, a == c, b == c) // true false false
    d := [3]int{1, 2}
    fmt.Println(a == d) // 编译错误
    

 

数组类型的不足

由于数组类型变量一旦声明后长度就固定了,这意味着我们将不能动态添加元素到数组,如果要这么做的话,需要先创建一个容量更大的数组,然后把老数组的元素都拷贝过来,最后再添加新的元素,如果数组长度很大的话,势必会影响程序性能。
另外,数组是值类型,这意味着作为参数传递到函数时,传递的是数组的值拷贝,也就是说,会先将数组拷贝给形参,然后在函数体中引用的是形参而不是原来的数组,当我们在函数中对数组元素进行修改时,并不会影响原来的数组,这种机制带来的另一个负面影响是当数组很大时,值拷贝会降低程序性能。
综合以上因素,我们迫切需要一个引用类型的、支持动态添加元素的新「数组」类型,这就是下面要介绍的切片类型,实际上,我们在 Go 语言中很少使用
数组,大多数时候会使用切片取代它。

 

切片(slice)

切片同样表示多个同类型元素的连续集合,但切片的长度是可变的,由于长度是可变的,所以可以解决数组长度在数据个数不确定情况下浪费内存的问题。

切片本身并不存储任何元素,而只是对现有数组的引用,(即slice本身没有数据,是对底层array的一个view),所以切片是引用类型。

	arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
	s := arr[2:6]
	s[0] = 10
	fmt.Println(arr)// [0 1 10 3 4 5 6 7]

 

切片是一个结构体,包含三个成员变量:指针、长度和容量。

type slice struct {
	array unsafe.Pointer // 元素指针
	len   int            // 长度
	cap   int            // 容量
}
  1. 指针:指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。
  2. 长度:切片中实际存在的元素个数。
  3. 容量:从切片的起始元素开始到其底层数组中的最后一个元素的个数。

在这里插入图片描述

 

go语言函数传参,传的都是值,即传切片会把切片的{arrayPointer, len, cap}这3个字段拷贝一份传进来。由于传的是底层数组的指针,所以可以直接修改底层数组里的元素。

func update_slice(s []int) {
	s[0] = 888
}
s := []int{1, 2, 3}
update_slice(s)
fmt.Printf("s=%v\n", s)	// [888 2 3]

 

切片的长度和容量都是不固定的,可以通过追加元素使切片的长度和容量增大。

切片主要有三种生成方式:

  1. 从数组生成一个新的切片
  2. 从切片生成一个新的切片
  3. 直接生成一个新的切片

 

从数组/切片生成一个新的切片

格式如下:

slice [开始位置:结束位置]

例:

	var student = [...]string{"Tom", "Ben", "Peter"}
	var student1 = student[1:3] // 从数组生成一个新的切片
	var student2 = student1[0:1] // 从切片生成一个新的切片
	fmt.Println("student数组:", student)
	fmt.Println("student1切片:", student1)
	fmt.Println("student2切片:", student2)
	fmt.Println("student数组地址为:", &student[1]) // 这里我们取student[1]元素的地址
	fmt.Println("student1切片地址为:", &student1[0])
	fmt.Println("student2切片地址为:", &student2[0])
	fmt.Println("student1切片长度为:", len(student1))
	fmt.Println("student1切片容量为:", cap(student1))
	fmt.Println("student2切片长度为:", len(student2))
	fmt.Println("student2切片容量为:", cap(student2))

上面代码运行结果为:

在这里插入图片描述

根据运行结果,我们可以归纳出从数组或切片生成新的切片有如下特性:

  1. 新生成的切片长度:结束位置 - 开始位置
  2. 新生成的切片取出的元素不包括结束位置对应的元素
  3. 新生成的切片是对现有数组或切片的引用,其地址与截取的数组或切片开始位置对应的元素地址相同。
  4. 新生成的切片容量指从切片的起始元素开始到其底层数组中的最后一个元素的个数。

 

直接生成一个新的切片

声明切片
var 切片变量名 []元素类型

 

初始化切片
  1. 在声明的同时初始化

    var student = []string{"Tom", "Ben", "Peter"} // len = cap = 3
    
  2. 使用make()函数初始化

    1. 声明完切片后,可以通过内建函数make()来初始化切片,格式如下:

      make ([]元素类型, 切片长度, 切片容量)
      

      对于切片的容量应该有个大概的估值,若容量值过小,对切片的多次扩充会造成性能损耗。

    2. 例:

      var student []int
      student = make([]int, 2, 10) // student切片在初始化后,自动填充0值
      
  3. 二维切片的初始化

    s2d := [][]int {
        {1},{2,3}, // 二维数组每一行的长度相等,但二维切片各行的长度可以不等
    }
    
     s := make([][]int, n)
     for i := 0; i < len(s); i++ {
     	s[i] = make([]int, n)
     }
    

 

slice的扩展

	arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
	s1 := arr[2:6]
	s2 := s1[3:5]

可以思考一下s1的值是什么?s2的值又是什么呢?

在这里插入图片描述

  1. s1的值为[2 3 4 5],s2的值为[5 6]
  2. slice可以向后扩展,不可以向前扩展
  3. s[i]其中i不可以超越len(s),向后扩展不可以超越底层数组cap(s)

 

为切片添加元素

Go语言中,我们可以使用append()函数来对切片进行元素的添加。

	var student = [...]string{"Tom", "Ben", "Peter"}
	var student1 = student[0:1]
	fmt.Println("student数组:", student)
	fmt.Println("student1切片:", student1)
	student1 = append(student1, "Danny") // 对student1切片的元素添加,会覆盖引用数组对应的元素
	fmt.Println("扩充Danny后的student1切片:", student1,", 切片长度为:", len(student1),
		",切片容量为:",cap(student1))
	fmt.Println("扩充Danny后的student数组:", student)

在这里插入图片描述

为student1切片添加元素会覆盖引用数组对应的元素,所以如果切片是从其他数组或切片生成,新切片的元素添加需要考虑对原有数组中的数据的影响。

当切片不能再容纳其他元素时(即当前切片长度值等于容量值),下一次使用append()函数对切片进行元素添加,系统会重新分配更大的底层数组,容量会按2倍(cap < 1024)或1.25倍(cap > 1024)进行扩充,把原内存空间的数据拷贝过来,在新内存空间上执行append操作。(具体扩容机制需要结合源码分析)

    s := make([]int, 3, 5)
    for i := 0; i < 3; i++ {
        s[i] = i + 1
    } //s=[1,2,3]
    fmt.Printf("s[0] address %p, s=%v\n", &s[0], s)
    /*
       capacity还够用,直接把追加的元素放到预留的内存空间上
    */
    s = append(s, 4, 5) //可以一次append多个元素
    fmt.Printf("s[0] address %p, s=%v\n", &s[0], s)
    /*
       capacity不够用了,得申请一片新的内存,把老数据先拷贝过来,在新内存上执行append操作
    */
    s = append(s, 6)
    fmt.Printf("s[0] address %p, s=%v\n", &s[0], s)

在这里插入图片描述

 

从切片删除元素

由于Go语言没有为删除切片元素提供方法,所以需要我们手动将删除元素点前后的元素连接起来,从而实现对切片中元素的删除

	var student = []string{"Tom", "Ben", "Peter", "Danny"}
	student = append(student[0:1], student[2:]...) // 与下一行注释代码等价
	// student = append(student[0:1], student[2], student[3])
	fmt.Println("student切片:", student) // [Tom Peter Danny]
	fmt.Println("student切片长度:", len(student)) // 3
	fmt.Println("student切片容量:", cap(student)) // 4

 

copy函数的使用

切片拷贝的基本语法:

copy(切片1, 切片2)

例:

	srcSlice := []int{1, 2}
	dstSlice := []int{6, 6, 6, 6, 6, 6}
	copy(dstSlice, srcSlice)
	fmt.Println("dst = ", dstSlice)	// [1 2 6 6 6 6]
	
	srcSlice = []int{1, 2}
	dstSlice = []int{6, 6, 6, 6, 6, 6}
	copy(srcSlice, dstSlice)
	fmt.Println("src = ", srcSlice)	// [6, 6]

通过上面这个程序,我们可以看出,使用copy函数进行切片拷贝时,拷贝的长度为两个slice中长度较小的长度值。

 

字典、映射(map)

映射是一种无序的键值对的集合,map的键类似于索引,指向数据的值。

 

声明映射

格式如下:

var map [键类型]值类型

键的类型,必须是支持==和!=操作符的类型,映射、切片、函数以及包含上述三种类型的结构体不能作为map的键,使用这些类型会导致编译错误。

获取元素:m[key]。

  1. 当key不存在时,获得value类型的默认值
  2. 用value, ok := m[key] 来判断是否存在key
  3. key是唯一的,添加重复的key会覆盖之前的元素

 

初始化映射

  1. 在声明的同时初始化

    var studentScoreMap = map[string]int {
        "Tom" : 80,
        "Ben" : 85,
        "Peter" : 90,
    }
    
  2. 使用make()函数初始化

    make(map[键类型]值类型, map容量)
    

    使用make()函数初始化map时可以不指定map容量,但是对于map的多次扩充会造成性能损耗。

    字典中不能使用cap()函数,只能使用len()函数,len()函数返回map拥有的键值对的数量。

 

从映射中删除键值对

Go语言通过delete()函数来对map中的指定键值对进行删除操作。格式如下:

delete(map实例,)
  1. 如果key存在,执行删除元素
  2. 如果key不存在,map中内容不变,也不会报错

Go语言没有为map提供清空所有元素的方法。

 

map的遍历

使用range遍历key,value对,或者单独遍历key:

	m1 := map[int]string{1:"Luffy", 2:"Sanji"}

	// 遍历1,第一个返回值是key,第二个返回值是value
	for k, v := range m1 {
		fmt.Printf("%d ----> %s\n", k, v)
        // 1 ----> Luffy
		// 2 ----> Sanji
	}

	// 遍历2,第一个返回值是key,第二个返回值是value(可省略)
	for k := range m1 {
		fmt.Printf("%d ----> %s\n", k, m1[k])
        // 1 ----> Luffy
		// 2 ----> Sanji
	}

map的遍历顺序是随机的,所以每次遍历的结果可能都不相同。如需特定顺序,需手动对key或者value进行排序。

 

字典排序

我们已经知道 Go 语言的字典是一个无序集合,如果你想要对字典进行排序,可以通过分别为字典的键和值创建切片,然后通过对切片进行排序来实现。

按照键进行排序
	testMap := map[string]int{"three": 3, "two": 2, "one": 1}

	keys := make([]string, 0)
	for k := range testMap {
		keys = append(keys, k)
	}
	sort.Strings(keys) // 对键进行排序
	fmt.Println("Sorted map by key:")
	for _, k := range keys {
		fmt.Println(k, testMap[k])
	}

在这里插入图片描述

 

按照值进行排序
	testMap := map[string]int{"three": 3, "two": 2, "one": 1}

	values := make([]int, 0)
	for _, v := range testMap {
		values = append(values, v)
	}

	// 键值对调
	invMap := make(map[int]string, 3)
	for k, v := range testMap {
		invMap[v] = k
	}

	sort.Ints(values) // 对值进行排序
	fmt.Println("Sorted map by value:")
	for _, v := range values {
		fmt.Println(invMap[v], v)
	}

在这里插入图片描述

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐