Go数组
内部实现
在Go语言里面,数组是长度固定的数据类型,必须存储一段相同类型的元素,而且这些元素是连续的。我们这里强调固定长度,可以说是和切片最明显的区别。
数组存储的类型可以是内置类型,比如整型或者字符串,也可以是自定义的数据结构。因为是连续的,所以索引比较好计算,所以我们可以很快的索引数组中的任何数据。
这里的索引,一直都是0,1,2,3这样的,因为其元素类型相同,我们也可以使用反射,获取类型占用大小,进行移位,获取相对应的元素。
声明和初始化
var array [5]int
这里声明了一个数组array
,但是并没有进行初始化赋值,这时数组array
里面的值,默认赋值为整型的零值。
数组一旦声明后,数组里存储的数据类型和数组长度就都不能改变了。如果需要存储更多的元素,就需要创建另一个更长的数组,再把原来数组的值复制到新数组里面。
对已被默认初始化为零值的数组,再次初始化
var array [5]int
array = [5]int{10, 20, 30, 40, 50}
让Go自动计算声明数组的长度
array := [...]int{10, 20, 30, 40, 50}
使用:=
操作符声明数组时并指定特定元素的值
array := [5]int{10, 20, 30, 40, 50}
让特定索引值为零
array := [5]int{0,1,3,0,0} // 0,3,4索引对应的值为0
array:=[5]int{1:1,3:4} // 只初始化索引1和3的值
使用数组
因为数组内存分布是连续的,所以数组是效率很高的数据结构。可以通过使用[]
操作符访问数组里某个单独元素。
array := [5]int{1, 2, 3, 4, 5}
array[2] = 20
fmt.Printf(array[2])
访问指针数组元素。
array := [5]*int{0:new(), 1:new()} // 声明包含5个元素的指向整型的数组,并用整型指针初始化索引为0,1的数组元素
// 为索引0,1的元素赋值
*array[0] = 10
*array[1] = 20
把同类型的一个数组赋值给另外一个数组
var newarray [5]string
array := [5]string{"Red", "Blue", "Green", "Yellow", "Pink"}
newarray = array
把指针数组赋值给另一个
var newarray [3]*string
array := [3]*string{new(string), new(string), new(string)}
*array[0] = "Red"
*array[1] = "Blue"
*array[2] = "Green"
newarray = array
多维数组的声明和初始化
声明二维数组
var array [4][2]int
array := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
array := [4][2]int{1: {10, 11}, 3: {20, 21}} // 声明并初始化外层数组中索引1,3元素
array := [4][2]int{1: {0: 20}, 3: {1: 22}} // 声明并初始化外层和内层数组的元素
在函数间传递数组
在函数间传递变量时,总是以值的方式传递的。如果这个变量是一个数组,意味着整个数组,不管有多长,都会完整复制,并传递给函数,这会是一个开销很大的操作。
func main() {
var array [1e6]int // 声明一个8MB的数组
transfer(array)
fmt.Println(array)
}
func transfer(a [1e6]int) {
a[0] = 99999
fmt.Println(a)
}
通过输出a结果可以看到数组是复制的,原数组并没有发生改变。
为了减少复制带来的开销,可以使用指针在函数间传递大数组
func main() {
var array [1e6]int // 声明一个8MB的数组
transfer(&array)
fmt.Println(array)
}
func transfer(a ×[1e6]int) {
a[0] = 99999
fmt.Println(a)
}
因为现在传递的是指针,所以改变指针指向的值,会改变共享的内存。
这里注意,数组的指针和指针数组是两个概念,数组的指针是[5]int,指针数组是[5]int,注意*的位置。
Go切片
内部实现
切片是一种数据结构,这种数据结构便于使用和管理数据集合。切片是围绕动态数组的概念构建的,可以按需自动增长和缩小,其底层内存也是在连续块中分配的,效率非常高,可以通过对切片再次切片来缩小一个切片的大小,还可以通过索引获得数据、迭代以及垃圾回收优化的好处。
切片是一个很小的对象,对底层数组进行了抽象,切片是只有三个字段的数据结构:指向底层数组的指针、切片访问的元素个数(即长度)和切片允许增长到的元素个数(即容量)。
声明和初始化
使用长度声明一个字符串切片
slice := make([]string, 5) // 创建一个字符切片,长度和容量都是5
使用长度和容量声明整形切片,分别指定长度和容量时,声明切片,底层数组的长度是指定的容量,但是初始化后并不能访问所有的元素。
slice := make([]string, 3, 5) // 其长度为3个元素,容量为5个元素
容量必须 >= 长度,不能创建长度 > 容量的切片
通过切片字面量来声明切片
slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"}
使用索引声明切片
slice := []string{99: ""} // 使用空字符串初始化第100个元素
切片声明时不需要在[]操作符里声明长度
nil和空切片
在声明时不做任何初始化,就会创建一个nil切片
var slice []int
声明空切片
slice := make([]int, 0) // 使用make创建空的整形切片
slice := []int{} // 使用字面量创建空的整型切片
nil切片和空切片,它们的长度和容量都为0,但它们指向的底层数组的指针不一样,nil切片意味着指向底层数组的指针为nil,而空切片对应的指针是地址。
使用切片
使用切片字面量来声明切片
slice := []int{10, 20, 30, 40, 50}
slice[1] = 25
使用切片创建切片
slice := []int{10, 20, 30, 40, 50}
newSlice := slice[1:3]
这两段切片共享同一段底层数组,所以当修改的时候,底层数组的值就会发生改变,同时原切片的值也改变了。
如何计算新的切片的长度和容量
对底层数组容量为k的切片slice[i:j]来说
长度: j - i
容量: k - i
使用3个索引创建切片
source := []string{"Apple", "orange", "Plum", "Banana", "Grape"}
slice := source[2:3:4]
这样创建了长度为1,容量为2的切片
如何计算3个索引创建的切片的长度和容量
对底层数组容量为k的切片slice[i:j:k]来说
长度: j - i
容量: k - i
切片增长
使用append向切片增加元素。
slice := []int{10, 20, 30, 40, 50}
newSlice := slice[1:3]
newSlice = append(newSlice, 60)
使用append同时增加切片的长度和容量
slice := []int{10, 20, 30, 40}
newSlice := append(slice, 50)
append函数会智能地处理底层数组的容量增长,在切片的容量小于1000个元素时,总是成倍地增长容量,一旦切片的容量超过1000个,容量的增长因子会设为1.25,也就是每次增长25%的容量。
将一个切片追加到另一个切片
s1 := []int{1, 2}
s2 := []int{3, 4}
fmt.Println(append(s1, s2))
Output:
[1 2 3 4]
迭代切片
使用for range迭代切片
range创建了每个元素的副本,而不是直接返回对该元素的引用。
slice := []int{10, 20, 30, 40}
for index, value := range slice {
fmt.Printf("Index: %d Value: %d\n", index, value)
}
如果不需要索引值,可以使用占位字符(下划线_
)来忽略索引值
slice := []int{10, 20, 30, 40}
for _, value := range slice {
fmt.Printf("Index: %d Value: %d\n", index, value)
}
使用传统的for循环对切片进行迭代
slice := []int{10, 20, 30, 40}
for index := 2, index < len(slice) {
fmt.Printf("Index: %d Value: %d\n", index, slice[index])
}
多维切片
声明多维切片
slice := [][]int{{10}, {100, 200}}
slice[0] = append(slice[0], 20) // 组合切片的切片
在函数间传递切片
在函数间传递切片就是要在函数间以值的方式传递切片。由于切片的尺寸很小,在函数间复制和传递切片成本也很低。
func main() {
slice := make([]int, 1e6) // 分配包含100万个整型值的切片
fmt.Printf("%p\n",&s)
transfer(slice)
fmt.Println(slice)
}
func transfer(s []int) {
fmt.Printf("%p\n",&s)
s[0] = 99999
fmt.Println(s)
}
0x40c0e0
0x40c0f0
输出的两个切片对应地址不一样,可以确认切片在函数间传递是复制的。而我们修改一个索引的值后,原切片的值也被跟着修改了,说明它们共用一个底层数组。
Go映射
内部实现
映射是一种数据结构,用于存储一系列无序的键值对。映射基于键来存储值,键就像索引一样,可以快速检索数据,键指向与该键关联的值。
映射是一个集合,使用了散列表来实现,可以使用类似处理数组和切片的方式迭代映射中的元素。但映射是无序的集合,意味着没有办法预测键值对被返回的顺序。
映射的散列表包含一组桶,每次存储和查找键值对的时候,都要先选择一个桶。把操作映射时指定的键传给映射的散列函数,就能选中对应的桶。这个散列函数的目的是生成一个索引,这个索引最终将键值对分布到所有可用的桶里。
这种方式的好处在于,存储的数据越多,索引分布越均匀,所以我们访问键值对的速度也就越快。
声明和初始化
使用make声明映射
dict := make(map[string]int) // 创建空映射
dict := map[string]int{"Red": "#da1337", "Orange": "#e95a22"} // 给空映射赋值
使用映射字面量声明空映射
dict := map[string]int{}
通过声明映射创建一个nil映射
var dict map[string]int // 声明nil映射
dict = make(map[string]int) // 给nil映射分配内存空间
dict["A"] = 1 // 给nil映射初始化赋值
使用映射
从映射获取值并判断键是否存在
values, exists := colors["Blue"]
if exists {
fmt.Println(value)
}
从映射中删除一项
delete(dict, "Blue")
使用range迭代映射
dict := map[string]int{"Red": "#da1337", "Orange": "#e95a22"}
for key, value := range dict {
fmt.Println(key, value)
}
在函数间传递映射
在函数间传递并不会制造出该映射的一个副本,当传递映射给一个函数,并对这个映射做了修改,所有对这个映射的引用都会察觉到这个修改。
func main() {
dict := map[string]int{"Red": "#da1337", "Orange": "#e95a22"}
transfer(dict)
fmt.Println(dict)
}
func transfer(d map[string]int) {
s["Red"] = "#fffff"
fmt.Println(dict)
}
部分引用了飞雪无情大佬的博客 http://www.flysnow.org/