前言
任何语言在处理实际问题的时候,都需要与数据源交互,go语言提供了三种数据结构管理数据:数组,切片和映射
数组
首先是数组,与其他语言类似,go的数组长度固定,可以存储内置类型和结构类型,数组的内存连续,因而cpu可以长久缓存它,这样在遍历时速度也更快,由于元素类型相同且移动距离一定,所以索引速度也就快,如下图
数组声明时长度在类型前
var array [5]int
长度是固定的,如果要扩容只能再创建一个新的,在把原数组复制过去,数组初始化后,默认初始值是类型的零值,若要赋值
array: [5]int{10,20,30,40,50}
也可以不给长度值,自动推断长度
array: [...]int{10,20,30,40,50}
可以指定部分初始值,其他为空
array: [5]int{1:0,2:0}
访问数组时,利用索引,从0开始
- 指针数组
array : =[5] *int {0:new(int),1:new(int)}
初始化则为指向整型
*array[0] = 1
指针数组赋值时不复制实际值,只复制引用地址
如图是两个赋值后的数组,指向同一组值
- 数组的互相赋值,只有在长度和类型都相同时才可以
- 多维数组
多维数组声明
var array [4][2]int
初始化
访问或为元素赋值需要提供多个维度
array[a][b] = c
也可也访问一个子维度的数组
var a[2] int = array[1]
- 函数传参时数组的使用
在函数中传参如果用数组传,开销会相当大,在函数被调用时,会在栈上分配对应数组大小内存,然后把值复制进来,可以使用指针来指向数组,传参时使用指针替代,这样就节省了大量栈内存
// 分配一个需要 8 MB 的数组
var array [1e6]int
// 将数组的地址传递给函数 foo
foo(&array)
// 函数 foo 接受一个指向 100 万个整型值的数组的指针
func foo(array *[1e6]int) {
...
}
这样操作节约内存,而且性能更好,但是由于指针指向的是原值,如果改变这个值,就会造成其他指向这块值的指针也拿到了变化后的值,这时候,就需要使用到切片
切片
切片的核心概念是动态数组,可以通过内置函数append按需分配大小,同时它不同于其他语言中的链式结构,切片也是内存连续的,享有索引,迭代以及垃圾回收优化的好处
- 内部实现
切片内部还是数组,它有一个指针指向底层数组,同时还有一个长度和最大容量
- 创建和初始化
可以使用make函数创建切片,参数为切片长度和容量,如果不指定容量,默认和长度相同,长度不可以大于容量
// 其长度和容量都是 5 个元素
slice := make([]string, 5)
// 其长度为 3 个元素,容量为 5 个元素
slice := make([]int, 3, 5)
也可以创建时就初始化
// 其长度和容量都是 5 个元素
slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"}
//利用索引局部初始化
slice : = []string{99:""}
- 数组与切片的不同:在[ ]运算符里指定了一个值,就是数组,空的就是切片
// 创建有 3 个元素的整型数组
array := [3]int{10, 20, 30}
// 创建长度和容量都是 3 的整型切片
slice := []int{10, 20, 30}
- nil切片,空切片
如果声明时不初始化,就会创建一个nil切片
创建nil切片
var slice []int
创建空切片
使用make函数
slice := make([]int, 0)
使用切片字面变量声明切片
slice := []int{10,20,30,40,50}
// 创建一个新切片
// 其长度为 2 个元素,容量为 4 个元素
newSlice := slice[1:3]
新切片指向旧的第一个元素,对于它来说,访问不到旧切片的[1]元素之前的元素
-长度和容量的计算
对底层数组容量是 k 的切片 slice[i:j]来说
长度: j - i
容量: k - i
也就是说容量就是切片头后面的内容,长度就是这个切片自身长度,如果修改长度外的元素就会引起异常,除非append扩容
把容量扩到长度里
如上图,newSlice扩容后长度加1
如上图,如果长度扩容后超过容量,会创建一个长度足够的新底层数组,将原有底层数组copy进来,之后再追加新值,这里的增长规则是1000长度内翻倍,超过1000就每次增加25%
- 创建切片时还可以使用第三个参数,索引,它可以控制新切片的容量 ,但是设置索引不能超出容量,否则会报错
- 容量和长度一样的好处有,可以保证append即设立新底层数组,这样就不会与原数组发生内容重叠或者更改原数组内容
如下两图是容量长度不一致和一致下的情况,(切片就相当于从头尾元素左侧边缘切了一下,这样比较形象)
- 切片追加到切片
切片追加就是相当于合并,比如3,4追加到1,2 就会变成1,2,3,4 - 迭代切片
go中提供range关键字配合for进行迭代
slice :=[]int{1,2,3,4,5}
for index,value := range slice {
fmt.Printf("Index: %d Value: %d\n", index, value)
}
也可以用下划线替代索引
for _, value := range slice {
fmt.Printf("Value: %d\n", value)
}
也有传统for循环方式
for index := 2; index < len(slice); index++ {
fmt.Printf("Index: %d Value: %d\n", index, slice[index])
}
明显前两种比较简单
多维切片
切片和数组一样也有多维版本
slice:=[][]{{10},{100,200}}
多维切片append
slice[0] = append(slice[0], 20)
函数间传递切片
切片尺寸很小,在函数间传递成本低,在 64 位架构的机器上,一个切片需要 24 字节的内存:指针字段需要 8 字节,长度和容量字段分别需要 8 字节,切片传递时传递的是切片自身,不涉及底层数组
映射内部实现,基本功能
映射内部是键值对,可以基于键检索数据,同时内部是无序的,迭代时无法预测返回元素顺序,内部利用了散列表实现
这种散列就是把键值对放在这些桶里,在查找时可以减少定位次数,对于1000个元素,8次左右就可以定位到,而不需要最差查找1000次
映射内部主要有两个数据结构,第一个是一个数组,存储选择桶用的高八位,第二个是一个字节数组,存储所有的键后,又存了所有的值
总结就是:映射是一个存储键值对的无序集合
创建初始化
//使用make函数创建,键类型string,值类型int
dict :=make(map(string)int)
//创建同时初始化,键值同为string
dict := map[string]string{"Red": "#da1337", "Orange": "#e95a22"}
如下,切片也可以分别作为键或者值
dict := map[[]string]int{}
dict := map[int][]string{}
如果未初始化,则是一个nil映射,nil映射不能赋值,否则会报error
如下可以判断键是否存在,同时返回值
value, exists := colors["Blue"]
if exists {
fmt.Println(value)
}
或者判断值是否存在,返回键
value := colors["Blue"]
// 这个键存在吗?
if value != "" {
fmt.Println(value)
}
- 如果要迭代映射,可以使用range
colors := map[string]string{
"A":"1",
"B":"2"
}
for key,value :=range colors{
fmt.Printf("Key: $s Value: %s\n",key,value)
}
- 如果要删除映射元素,可以用delete
delete(colors,"A")
函数间传递映射
在函数间传递映射不会制造副本,任何操作都会影响到映射的内容,并且对映射的引用也会随之变动
这一点类似于切片,保证了低成本的复制
后记
这一章内容很多,之后会补充上来,之前两章内容似乎也有些问题,之后会修改重新发布