7、容器

数组

声明

数组是一个由固定长度的特定类型元素组成的序列,属于值类型(所以传参数组的时候一般使用指针,避免值类型传参复制导致内存消耗),不可以与nil比较,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,所以在Go语言中很少直接使用数组。

var variable [length]Type
// 声明
var arr [3]int
// 声明并赋值
var arr [3]int = [3]int{1,2,3}
// 自动类型推导
arr := [3]int{1,2,3}
// 自动类型推导并自动计算长度
arr := [...]int{1,2,3}
// 初始化并指定索引的值
arr := [5]string{2: "lucy", 4: "tom"}

数组的每个元素都可以通过索引下标来访问,索引下标的范围是从 0 开始到数组长度减 1 的位置,内置函数 len() 可以返回数组中元素的个数。

//main包
package main

import (
	"fmt"
)

func main() {
	var arr [3]int
	fmt.Println(len(arr)) //数组长度:3
	fmt.Println(arr[0])   //数组索引为0的元素值为:0
}

比较数组是否相等

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

//main包
package main

import (
	"fmt"
)

func main() {
	a1 := [...]int{1, 2, 3}
	a2 := [...]int{1, 2, 3}
	a3 := [...]int{2, 3, 4}
	a4 := [...]int{1, 2, 3, 4}
	fmt.Println(a1 == a2) //true
	fmt.Println(a1 == a3) //false
	fmt.Println(a1 == a4) //编译错误,[3]int不能和[4]int进行比较
}

遍历数组

//main包
package main

import "fmt"

func main() {
	names := [...]string{"lucy", "tom", "lily"}
	//使用for,根据数据长度遍历
	for i := 0; i < len(names); i++ {
		fmt.Printf("第%d个姓名为:%s\n", i, names[i])
	}
	//使用for range
	for index, value := range names {
		fmt.Printf("第%d个姓名为:%s\n", index, value)
	}
}

多维数组

var array_name [size1][size2] array_type
//声明
var ab [2][3]int
//声明并赋值
var ab [2][3]int = [2][3]int{{1, 2, 3}, {4, 5, 6}}
//自动类型推导
ab := [2][3]int{{1, 2, 3}, {4, 5, 6}}
//初始化并指定索引的值
ab := [5][3]int{2:{1, 2, 3}, 4:{1:66}}

//获取指定索引的值
a := ab[0][2]

注意:多维数组只有最外层可以使用[...]来表示数组长度,内层数组不可以使用[...]来表示数组长度

切片Slice

切片(slice)本质就是对一个底层数组的一个连续片段的引用(封装),所以切片是一个引用类型(因此更类似于 C/C++中的数组类型,或者Python中的 list 类型),这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集,需要注意的是,终止索引标识的项不包括在切片内,也就是切片不含结束索引的元素切片一般用于快速地操作一块数据集合

image-20240118152914721

容量和长度的区别

切片的长度(通过len(s)获取)就是它所包含的元素个数,这些元素是会被初始化的。

切片的容量(通过cap(s)获取)是从它的第一个元素开始数,到其底层数组元素末尾的个数。

也就是说,对于s := make([]int, 3, 6) ,该切片创建了一个能够容纳6个元素(容量)的数组。同时,因为长度length被设置成了3,所以,Go仅仅初始化前3个元素。因为slice的元素是[]int类型,所以前3个元素用int的零值0来初始化。剩余的元素空间只被分配,但没有使用。

从数组或切片生成新的切片

切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。

s := target[开始位置 : 结束位置]
package main

import "fmt"

func main() {
	a := [5]int{1, 2, 3, 4, 5}
	slice := a[1:3]
	fmt.Println(slice)                  // [2 3]
	fmt.Println(len(slice), cap(slice)) // 2 4

	a[1] = 10
	fmt.Println(slice) // [10 3]
}

从数组或切片生成新的切片拥有如下特性:

直接声明新的切片

注意[]type声明的是切片,是引用类型;而[...]type[length]type声明的是数组,是值类型

var name []Type
//声明一个切片,此时slice没有分配内存,是nil,len和cap都是0
var slice []string
//声明切片并赋值
var slice []string = []string{"lucy", "tom", "lily"}
//自动类型推导
slice := []string{"lucy", "tom", "lily"}
//声明一个空切片,此时slice已经分配内存,不是nil
slice := []string

make

make( []Type, len, cap )
package main

import "fmt"

func main() {
	a := make([]int, 2)         // len = 2;cap = 2
	b := make([]int, 2, 10)     // len = 2;cap = 10
	fmt.Println(len(a), len(b)) // 2 2
	//cap函数可以获取切片的容量
	fmt.Println(cap(a), cap(b)) // 2 10
}

注意:使用 make() 函数生成的切片一定发生了内存分配操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作,也就是说如果修改切片元素,那么相当于修改被切片的数组或切片的元素

package main

import "fmt"

func main() {
	a := [...]int{1, 2, 3, 4, 5, 6, 7}
	slice := a[1:6]
	slice[1] = 100
	fmt.Println(a) //[1 2 100 4 5 6 7]
}

append

Go语言的内建函数append()可以为切片动态添加元素,切片的扩容也只能通过append()方法进行

注意append()函数的第一个参数必须为slice;而第二位参数为可变参数,如果是切片则需要使用...进行解包,如果是数组就需要先转成切片再解包,例如append(a,arr[:]...)

package main

import "fmt"

func main() {
	var a []int
	//在切片尾部添加
	//在切片a的基础上,追加一个元素,并返回新的切片
	a = append(a, 1)
	//在切片a的基础上,追加三个元素,并返回新的切片
	a = append(a, 2, 3, 4)
	//在切片a的基础上,追加一个切片(这个切片需要使用省略号...进行解包),并返回新的切片
	a = append(a, []int{5, 6, 7}...)

	//在切片头部添加
	//在切片a的前面加一个元素0,并返回新的切片
	a = append([]int{0}, a...)
	//在切片a的前面加一个切片,并返回新的切片
	a = append([]int{-3, -2, -1}, a...)
  
	fmt.Println(a) // [-3 -2 -1 0 1 2 3 4 5 6 7]
}

注意:在使用append()函数为切片动态添加元素时,如果空间不足以容纳足够多的元素,切片就会进行扩容,此时新切片的长度会发生改变,切片在扩容时,容量的扩展规律是按现在容量(cap)的 2 倍进行扩充

每次扩容时,创建一个新的2倍容量的数组,并将之前数组内容copy过来,所以发生扩容的时候slice的内存地址会发生变化。

package main

import "fmt"

func main() {
	s := []int{100, 200, 300, 400}
	fmt.Println(len(s), cap(s)) // 4 4
	s = append(s, 500)
	fmt.Println(len(s), cap(s)) // 5 8
}

因为 append 函数返回新切片的特性,所以切片也支持链式操作,我们可以将多个 append 操作组合起来,实现在切片中间插入元素

// 在第i个位置插入x
a = append(a[:i], append(x, a[i:]...)...) 
package main

import "fmt"

func main() {
	slice := []int{1, 2, 3}
	insert(&slice, 1, 100, 200)
	fmt.Println(arr) // [1 100 200 2 3]
}

// 在切片slice的第index位插入num
func insert(slice *[]int, index int, num ...int) {
	*slice = append((*slice)[:index], append(num, (*slice)[index:]...)...)
}

copy

Go语言的内置函数 copy() 可以将一个切片复制到另一个切片中,如果加入的两个数组切片不一样大,就会按照其中len()较小的那个数组切片的元素个数进行复制。

copy( destSlice, srcSlice ) 

注意:目标切片必须和原切片类型一致

package main

import "fmt"

func main() {
	srcSlice := []int{100, 200, 300, 400}
	targetSlice := make([]int, 2, 5)
	i := copy(targetSlice, srcSlice)

	fmt.Println(i, targetSlice) // 2 [100 200]
}

删除元素

Go语言并没有对删除切片元素提供专用的语法或者接口,需要使用切片本身的特性来删除元素,根据要删除元素的位置有三种情况,分别是从开头位置删除、从中间位置删除和从尾部删除,其中删除切片尾部的元素速度最快

开头位置

a = a[n:] //从开头位置删除n个元素
package main

import "fmt"

func main() {
	a := []int{1, 2, 3, 4, 5}
	//从开头位置删除2个元素
	a = a[2:]
	fmt.Println(a) //[3 4 5]
}

中间位置

a = append(a[:n], a[m:]...) //从索引为n的位置,删除count=m-n个元素,即m=count+n
package main

import "fmt"

func main() {
	a := []int{1, 2, 3, 4, 5}
	//从索引为1的位置开始删除2个元素
	a = append(a[:1], a[2:]...)
	fmt.Println(a) //[1 3 4 5]
}

尾部位置

a = a[:len(a)-n] //删除尾部n个元素
package main

import "fmt"

func main() {
	a := []int{1, 2, 3, 4, 5}
	//删除尾部的2个元素
	a = a[:len(a)-2]
	fmt.Println(a) //[1 2 3]
}

注意:连续容器的元素删除无论在任何语言中,都要将删除点前后的元素移动到新的位置,随着元素的增加,这个过程将会变得极为耗时,因此,当业务需要大量、频繁地从一个切片中删除元素时,如果对性能要求较高的话,就需要考虑更换其他的容器了(如双链表等能快速从删除点删除元素)。

排序

Golang中除了自己手写排序算法实现排序外,还提供了sort

升序排序

package main

import (
	"fmt"
	"sort"
)

func main() {
	s1 := []int{4, 1, 6, 2, 10}
	sort.Ints(s1)
	fmt.Println(s1) // [1 2 4 6 10]

	s2 := []float64{3.1, 1.1, 2.5, 10.10}
	sort.Float64s(s2)
	fmt.Println(s2) // [1.1 2.5 3.1 10.1]

	s3 := []string{"a", "c", "f", "b", "e"}
	sort.Strings(s3)
	fmt.Println(s3) // [a b c e f]
}

降序排序

package main

import (
	"fmt"
	"sort"
)

func main() {
	s1 := []int{4, 1, 6, 2, 10}
	sort.Sort(sort.Reverse(sort.IntSlice(s1)))
	fmt.Println(s1) // [1 2 4 6 10]

	s2 := []float64{3.1, 1.1, 2.5, 10.10}
	sort.Sort(sort.Reverse(sort.Float64Slice(s2)))
	fmt.Println(s2) // [10.1 3.1 2.5 1.1]

	s3 := []string{"a", "c", "f", "b", "e"}
	sort.Sort(sort.Reverse(sort.StringSlice(s3)))
	fmt.Println(s3) // [f e c b a]

}

map

Go语言中 map 是一种特殊的数据结构,一种元素对(Pair)的无序集合,Pair 对应一个 key(索引)和一个 value(值),所以这个结构也称为关联数组或字典,这是一种能够快速寻找值的理想结构,给定 key,就可以迅速找到对应的 value。

声明

var mapName map[keyType]valueType // [keyType]和valueType中间可以有空格
// 声明一个map
var m map[int]string
// 声明map并初始化
var m map[int]string = map[int]string{1: "lucy", 2: "tom"}
// 自动类型推导
m := map[int]string{1: "lucy", 2: "tom"}
// 换行初始化时最后一个元素后要加逗号
m := map[int]string{
  1: "lucy",
  2: "tom",
  3: "lily",
}

// 使用make进行初始化
m := make(map[int]string)
// 使用make进行初始化,并且初始容量100
m := make(map[int]string, 100)
package main

import "fmt"

func main() {
	a := make(map[int]string)
	// 添加元素
	a[1] = "lucy"
	a[2] = "tom"
	a[3] = "lily"
	// 如果key已存在,那么会修改value
	a[1] = "james"

	// 获取值和是否存在,存在返回value,true
	val, exist := a[1]
	fmt.Println(val, exist) // james true

	// 使用一个变量接收,value
	value := a[1]
	fmt.Println(value) // james

	// 遍历map
	for k, v := range a {
		println(k, v)
	}
}

删除

使用 delete() 内建函数从 map 中删除一组键值对,delete() 函数的格式如下:

delete(map, key)
package main

import "fmt"

func main() {
	a := map[string]string{
		"A1": "tom",
		"A2": "lily",
		"A3": "james",
	}
	// 删除编号为A2的键值对
	delete(a, "A2")
	fmt.Println(a) // map[A1:tom A3:james]
}

切片与map组合

package main

import "fmt"

func main() {
	// map切片:切片中元素为map
	a1 := []map[string]string{
		{
			"id":   "1",
			"name": "tom",
		},
		{
			"id":   "2",
			"name": "lucy",
		},
	}
	fmt.Println(a1) // [map[id:1 name:tom] map[id:2 name:lucy]]

	// 切片作为map的value
	a2 := map[string][]string{
		"A1": {"tom", "lucy"},
		"A2": {"lily", "james"},
	}
	fmt.Println(a2) // map[A1:[tom lucy] A2:[lily james]]
}

sync.Map

Go 语言中map如果在并发读的情况下是线程安全的,如果是在并发写的情况下,则是线程不安全的。Golang 为我们提供了一个sync.Map 是并发写安全的。

Golang 中的 map 的 key 和 value 的类型必须是一致的,但 sync.Map 的 key 和 value 不一定是要相同的类型,不同的类型也是支持的。

sync.Map是一个结构体,所以无须初始化,直接声明即可使用,获取使用new()

声明

var smap sync.Map
smap = new(sync.Map)

常用API

Store()添加元素

//对一个Map添加any类型的key和value
func (m *Map) Store(key, value interface{})

Load()获取元素

//根据key获取value,ok表示是否存在
func (m *Map) Load(key interface{}) (value interface{}, ok bool)

Delete()删除元素

//根据key删除
func (m *Map) Delete(key interface{})

LoadAndDelete()删除并返回

//根据key删除,如果存在,就返回value,true;如果不存在返回nil,false
func (m *Map) LoadAndDelete(key any) (value any, loaded bool)

Range()遍历元素

//接受一个参数为any类型的key和value、返回值为bool类型的函数
//如果返回true表示继续遍历,如果false表示停止遍历
func (m *Map) Range(f func(key, value interface{}) bool)
a := new(sync.Map)
a.Store("lucy", "stu001")
a.Store("tom", "stu002")
a.Store("lily", "stu003")

a.Range(func(key, value any) bool {
    fmt.Printf("name:%s,id:%s\n", key, value)
    return true
})

LoadOrStore()获取或添加

//这个函数就是如果key存在那么返回value,true;如果key不存在就添加该key-value
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) 

list

列表是一种非连续的存储容器,由多个节点组成,节点通过一些变量记录彼此之间的关系,列表有多种实现方法,如单链表、双链表等。

在Go语言中,列表使用 container/list 包来实现,内部的实现原理是双链表,列表能够高效地进行任意位置的元素插入和删除操作。

该包下有两个结构体,Element(代表双向链表中的节点)和List(代表双向链表)

初始化

list 的初始化有两种方法:分别是使用 list.New() 函数(func New() *List)和 var 关键字声明,两种方法的初始化效果都是一致的。

li := list.New()
var li list.List

列表与切片和 map 不同的是,列表并没有具体元素类型的限制,因此,列表的元素可以是任意类型,但是最好放一样的类型

Element

成员 描述
Value 在节点中存储的值
func (e *Element) Next() *Element 返回该元素的下一个元素指针,如果没有则返回nil
func (e *Element) Prev() *Element 返回该元素的前一个元素指针,如果没有则返回nil

List

成员 描述
func (l *List) Back() *Element 获取list l的最后一个元素
func (l *List) Front() *Element 获取list l的第一个元素
func (l *List) Init() *List list l初始化或者清空
func (l *List) InsertAfter(v interface{}, mark *Element) *Element 在list l中元素mark之后插入一个值为v的元素,并返回该元素,如果mark不是list中元素,则list不改变
func (l *List) InsertBefore(v interface{}, mark *Element) *Element 在list l中元素mark之前插入一个值为v的元素,并返回该元素,如果mark不是list中元素,则list不改变。
func (l *List) Len() int 获取list l的长度
func (l *List) MoveAfter(e, mark *Element) 将元素e移动到元素mark之后,如果元素e或者mark不属于list l,或者e==mark,则list l不改变
func (l *List) MoveBefore(e, mark *Element) 将元素e移动到元素mark之前,如果元素e或者mark不属于list l,或者e==mark,则list l不改变
func (l *List) MoveToBack(e *Element) 将元素e移动到list l的末尾,如果e不属于list l,则list不改变
func (l *List) MoveToFront(e *Element) 将元素e移动到list l的首部,如果e不属于list l,则list不改变
func (l *List) PushBack(v interface{}) *Element 在list l的末尾插入值为v的元素,并返回该元素
func (l *List) PushBackList(other *List) /在list l的尾部插入另外一个list,其中l和other可以相等
func (l *List) PushFront(v interface{}) *Element 在list l的首部插入值为v的元素,并返回该元素
func (l *List) PushFrontList(other *List) 在list l的首部插入另外一个list,其中l和other可以相等
unc (l *List) Remove(e *Element) interface{} 如果元素e属于list l,将其从list中删除,并返回元素e的值
//main包
package main

import (
	"container/list"
	"fmt"
)

func main() {
	li := list.New()
	li.PushBack("lucy")
	li.PushBack("tom")
	li.PushBack("lily")

	fmt.Printf("列表的长度:%d\n", li.Len())
	//遍历li
	for e := li.Front(); e != nil; e = e.Next() {
		fmt.Println(e.Value)
	}
	/*
		列表的长度:3
		lucy
		tom
		lily
	*/
}

for range

range 关键字在 go 语言中是相当常用好用的语法糖,可以用在 for 循环中迭代 arrayslicemapchannel字符串所有涉及到遍历输出的东西。

注意:range迭代出来的value(v)只是每个元素的副本,也就是说range每迭代一次,就会出现一次拷贝,所以,修改v并不会对原有数据进行修改

如果不需要k或v,建议使用匿名变量_,这样减少赋值所带来的损耗

字符串

k:该字符开始位置的字节索引

v:单个字符输出的是ASCII码,实际类型是 rune 类型

package main

import "fmt"

func main() {
	a := "你好呀"
	for k, v := range a {
		fmt.Printf("第%d个字节后是:%c\n", k, v)
	}
	/*
		第0个字符是:你
		第3个字符是:好
		第6个字符是:呀
	*/
}

数组

k:元素的索引

v:元素的值

package main

import "fmt"

func main() {
	a := [...]string{"lucy", "lily", "tom"}
	for k, v := range a {
		fmt.Printf("第%d个姓名是:%s\n", k, v)
	}
	/*
		第0个姓名是:lucy
		第1个姓名是:lily
		第2个姓名是:tom
	*/
}

切片

k:元素的索引

v:元素的值

package main

import "fmt"

func main() {
	a := []string{"lucy", "lily", "tom"}
	for k, v := range a {
		fmt.Printf("第%d个姓名是:%s\n", k, v)
	}
	/*
		第0个姓名是:lucy
		第1个姓名是:lily
		第2个姓名是:tom
	*/
}

map

k:Pair的键(key)

v:Pair的值(value)

//main包
package main

import "fmt"

func main() {
	a := map[string]string{"A1": "tom", "A2": "lily", "A3": "james"}
	for k, v := range a {
		fmt.Printf("编号:%s,姓名:%s\n", k, v)
	}
	/*
		编号:A1,姓名:tom
		编号:A2,姓名:lily
		编号:A3,姓名:james
	*/
}

channel

v:从管道中接收的数据

ch := make(chan string)
for v := range ch {
    fmt.Println("accept ok:", v)
}

make和new的区别