Go数据结构-数组、切片和映射

Go数据结构-数组、切片和映射

Created: October 14, 2023 2:44 PM
Class: Golang
Type: 掌握
Reviewed: No

Go 语言中有三种数据结构可以让用户管理集合数据:数组、切片和映射。

这三种数据结构是 Go 语言核心的一部分,在标准库中广泛使用。

数组

数组是切片和映射的基础数据结构。

数组的底层实现

在其他语言中,数组的长度固定,存储元素的类型也相同。

Go 语言中,数组用于存储一段具有相同类型的元素的连续块,且其长度固定。

元素类型可以是内置类型——整型、字符串等,也可以是某种结构类型。(有关 Go 语言的所有类型会在之后讲解)

数组最基本的组成有「起始地址」和「偏移量」

「第 i 个元素的内存地址」=「起始地址」+ i * 「偏移量」

内存连续使的 CPU 能够把正在使用的数据缓存更久的时间,且可以更快速的迭代数组的元素、以固定速度索引数组的任意数据。

数组的声明和初始化

声明一个数组需要指定两个参数——数组内部存储的「数据类型」和「数组长度」

声明后的数组其数据类型和数组长度都不能再改变,变更任意一个都需要创建一个新的合适的数组再将数据复制到新数组里。

Go 语言声明变量时总会使用对应类型的零值来对变量进行初始化,数组也是。

数组初始化时,数组内的每个元素都会被初始化为对应类型的零值。

var arr1 [5]int
fmt.Println(arr1)
//-->[0 0 0 0 0]

数组声明后即会进行初始化;

也可以使用「字面量」进行初始化声明,使用「…」代替数组长度,Go 语言会根据初始化时数组字面量的数量确定数组长度,但是仅在字面量创建数组时使用。

arr2 := [5]int{1,2,3,4,5}
fmt.Println(arr2)
//-->[1 2 3 4 5]
arr3 := [...]int{1,2,3}
fmt.Println(arr3)
//-->[1 2 3]
var arr4 [...]int
fmt.Println(arr4)
//-->invalid use of [...] array (outside a composite literal)

既然数组的元素地址都能够被快速定义,那么在初始化数组时对指定位置元素赋值也是可以的。

arr5 := [...]int{3: 2, 10: 9}
//-->[0 0 0 2 0 0 0 0 0 0 9]

使用数组

使用[]运算符来访问数组里的元素,并按需求对其更改

arr6 := [...]int{1,2,3,4,5,6}
fmt.Println(arr6[0:5])
//-->[1 2 3 4 5]
arr6[5] = 0
fmt.Println(arr6)
//-->[1 2 3 4 5 0]

数组中也可以存储指针,然后使用*运算符就可以访问元素指针所指向的值。

arr7 := [5]*int{1: new(int), 2: new(int)}
*arr7[0] = 10
*arr7[1] = 20
fmt.Println(arr7)
//-->[0xc000020130 0xc000020138 <nil> <nil> <nil>]
fmt.Println(arr7[0])
//-->10

Go 语言中数组也是一个值,所以数组也可以进行赋值操作。

但是只有类型相同的数组才可以相互之间赋值,类型相同的数组是指:数组长度和数组元素的类型都相同的数组,对于存储指针的数组之间在进行赋值时,他们存储的指针相同,指针对应的值也是相同的。

arr7 := []*string{new(string), new(string), new(string), new(string)}
*arr7[0] = "blue"
*arr7[1] = "green"
*arr7[2] = "red"
*arr7[3] = "white"
arr8 := arr7
fmt.Println(arr7)
//-->[0xc000014260 0xc000014270 0xc000014280 0xc000014290]
fmt.Println(arr8)
//-->[0xc000014260 0xc000014270 0xc000014280 0xc000014290]

多维数组

多维数组也可以先声明进行默认初始化,也可以使用字面量来快速初始化。

var arr9 [3][2]int
arr10 := [3][2]int{0:{0:1},2:{1:2}}
fmt.Println(arr9)
//-->[[0 0] [0 0] [0 0]]
fmt.Println(arr10)
//-->[[1 0] [0 0] [0 2]]

多维数组的使用也是使用[]运算符,这里不再赘述

函数间的数组传递

函数间传递数组是一个很大的开销,因为在函数之间传递变量时,总是以值的方式传递的,如果变量是一个数组,不管数组多长都会完整的生成一个副本传递给函数,这显然不太好,所以我们可以在需要传递数组时尽可能地传递数组的指针。但是如果改变了指针指向的数组的元素值,就会改变共享的内存,或许会造成一些麻烦……使用切片会更好,嘿嘿。

切片

切片是 Go 源码实现的一种数据结构,使用动态数组的概念,使的数组更便于使用和管理。

由于切片的底层内存仍旧是连续块,所以切片仍能够获得索引、迭代以及为垃圾回收优化的好处。

内部实现

切片的数据结构有3 个字段,对底层数组进行了抽象,且提供了相关的操作方法。

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

sliceShow

  • array:指向底层数组的指针
  • len:切片长度
  • cap:切片容量

创建和初始化

能否提前知道切片需要的容量会决定以哪种方式创建切片。

make 和切片字面量

使用 make 函数可以指定切片的长度和容量,若是省缺容量,只指定长度,那么容量和长度相同,声明后的底层数组长度是切片长度,若是直接访问长度之外的位置即使容量足够仍旧会报越界错误。

不允许创建容量小于长度的切片。

slice1 := make([]int, 5)
//-->[0 0 0 0 0]
slice2 := make([]int, 3, 5)
//-->[0 0 0]
slice3 := make([]int, 5, 3)
//-->len larger than cap in make([]int)

使用切片字面量的方式能够快速声明且初始化切片,与数组的字面量声明很相似,只是不指定长度。如果在[]中指定了一个值,那么创建的就不是切片,而是数组。

slice4 := []int{10, 20, 30}
//-->[10 20 30]
slice5 := []int{2: 30}
//-->[0 0 30]

nil 和空切片

空切片的长度和容量都为 0,其底层数组不会被分配任何存储空间

var slice6 []int
//slice6 := []int{}
//slice6 := make([]int, 0)
//-->[]

使用切片

访问和更改切片元素与数组相似,使用[]运算符即可。

slice7 := []int{1, 2, 3, 4, 5}
fmt.Println(slice7[2])
//-->3
slice7[4] = 100
fmt.Println(slice7)
//-->[1 2 3 4 100]

从一个切片中创建一个新切片,他们共用同一段底层数组,只是不同切片看到数组的不同部分。

slice8 := []int{1, 2, 3, 4, 5}
slice9 := slice8[1:3]
fmt.Printf("slice8[1]的内存地址为: %p,slice8内的数据为:%d,slice8的容量为:%d\n", &slice4[1], slice4, cap(slice4))
//-->slice8[1]的内存地址为: 0xc000022158,slice8内的数据为:[1 2 3 4 5],slice8的容量为:5
fmt.Printf("slice9[0]的内存地址为: %p,slice9内的数据为:%d,slice9的容量为:%d\n", &slice5[0], slice5, cap(slice5))
//-->slice9[0]的内存地址为: 0xc000022158,slice9内的数据为:[2 3],slice9的容量为:4

slice9 无法看到 slice8[0:1]片段,容量也是 slice8 减去这一段后的容量,长度即为slice8[1:3]包含的元素数量。
由于两个切片共享一个底层数组,当一个切片修改了该底层数组的共享部分,另一个切片也能够感知到。


切片不能把容量合并到切片长度里,容量就没有任何用处,使用 append 函数来做合并操作。

但是 append 函数调用返回时,若是进行了扩容,会返回一个包含修改结果的新切片,且其容量的改变取决于被操作的切片的可用容量。

使用 append 函数时,若切片的可用容量不足够处理合并,则会扩容,在切片容量小于 1000 个元素时,增长因子是 2,元素个数大于 1000 时,增长因子变为 1.2,当然随着语言演变这种增长算法会有改变,append 的底层实现不是标准库的一部分。

slice10 := []int{1, 2, 3, 4, 5}
slice11 := append(slice10, )
fmt.Printf("slice10[0]的内存地址为: %p,slice10内的数据为:%d,slice10的容量为:%d\n", &slice10[0], slice10, cap(slice10))
//-->slice10[0]的内存地址为: 0xc0000b4098,slice10内的数据为:[1 2 3 4 5],slice10的容量为:5
fmt.Printf("slice11[0]的内存地址为: %p,slice11内的数据为:%d,slice11的容量为:%d\n", &slice11[0], slice11, cap(slice11))
//-->slice11[0]的内存地址为: 0xc0000b4090,slice11内的数据为:[1 2 3 4 5],slice11的容量为:5

使用 append 函数而没有超出切片容量,那么前后的切片仍会共享底层数组。

slice12 := make([]int, 1, 5)
slice12[0] = 10
slice13 := append(slice12, 20)
fmt.Printf("slice12[0]的内存地址为: %p,slice12内的数据为:%d,slice12的容量为:%d\n", &slice12[0], slice12, cap(slice12))
//-->slice12[0]的内存地址为: 0xc00012c0f0,slice12内的数据为:[10],slice12的容量为:5
fmt.Printf("slice13[0]的内存地址为: %p,slice13内的数据为:%d,slice13的容量为:%d\n", &slice13[0], slice13, cap(slice13))
//-->slice13[0]的内存地址为: 0xc00012c0f0,slice13内的数据为:[10 20],slice13的容量为:5

我们在创建切片时,使用三个索引指定切片起始地址、长度和容量。

fruit := []string{"Apple","Orange","Plum","Banana","Grape"}
slice14 := fruit[2:3:4]
fmt.Println(slice14)
fmt.Printf("slice14的内存地址为: %p,slice14的长度为:%d,slice14的容量为:%d\n", slice14, len(slice14), cap(slice14))
//-->[plum]
//-->slice14的内存地址为: 0xc000068070,slice14的长度为:1,slice14的容量为:2

其计算公式为:

对于slice[i:j:k][2:3:4]
长度: j – i 或 3 - 2 = 1
容量: k – i 或 4 - 2 = 2

向切片追加多个元素时可以使用「…」运算符,可以将一个切片的所有元素追加到另一个切片中。

slice15 := []string{"Hello "}
slice16 := []string{"W","o","r","l","d"}
fmt.Printf("%v\n", append(slice15,slice16...))
//-->[Hello  W o r l d]

切片的迭代也可以使用 range 关键字配合 for 来完成。

range 关键字会返回两个值,第一个是当前迭代到的索引位置,第二个是该位置对应元素的副本,若是只有一个值,其表示元素副本。

切片的内置函数 len 和 cap 可以返回切片的长度和容量,这里不再举例。

多维切片

slice17 := [][]int{{1:2},{0:1,2:3}}
fmt.Println(slice17)
//--> [[0 2] [1 0 3]]

多维切片的元素访问和变更,以及追加操作都基本一致,若是想要对切片 slice17 中的第二个切片追加元素,使用 append 函数为 slice17[1]赋值即可。

切片在函数间的传递

由于切片的真实大小只是三个索引的大小,所以在函数间复制和传递切片的成本很低。

比如在 64 位架构的机器上,一个切片需要 24 字节的内存(指针、长度、容量字段分别需要 8 字节)复制时只会复制切片本身,而不会涉及底层数组。

映射

映射时 Go 源码的一种数据结构,用于存储一系列无序的键值对。

映射是一个集合,但是是无序的,所以对映射进行迭代时,没有办法返回的键值对的顺序,无序的根本原因是映射的实现使用了散列表。

映射的散列表包含一组「桶」,当我们操作映射时需要指定「键」,「键」传入「散列函数」得出「散列值」,根据「散列值低位」选中对应的「桶」,根据「散列值高位」提高键值的分散性,然后键值对会最终被分布到所有可用的桶里。

创建和初始化

创建并初始化映射可以使用 make 函数,也可以使用映射字面量。

map1 := make(map[string]string, 2)
map2 := map[string]string{"Red": "apple", "Orange": "orange"}
map1["Red"] = "apple"
fmt.Println(map1)
//-->map[Red:apple]
fmt.Println(map2)
//-->map[Orange:orange Red:apple]

映射的键可以是任何值,只要这个值的类型可以使用==运算符做比较,具有引用语义的切片和函数不能作为映射的键。

使用映射

映射返回的参数有两个,第一个是值——即这个键所对应的值,第二个是一个布尔值——表示这个键对应的值是否存在。若是只返回一个值,则代表这个键对应的值

value, exist := map1["Red"]
fmt.Println(exist, value)
//-->true apple

可以使用 range 对映射进行迭代,但是由于每次存储时映射里键值对是乱序的,所以输出可能看起来也是乱序的,可以使用 delete 函数将一个键值对从映射里删除。

for key, value := range map2 {
		fmt.Println(key, value)
}
delete(map2, "Orange")
fmt.Println(map2)
//-->Red apple
//-->Orange orange
//-->map[Red:apple]

在函数间传递映射

在函数间传递映射并不会制造一个该映射的一个副本,所以在接受这个映射的函数里对这个映射进行的修改会被察觉到,这个特性和切片很相似。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值