Go语言实战笔记 数组、切片和映射

Go语言实战笔记 数组、切片和映射_第1张图片
封面

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/

你可能感兴趣的:(Go语言实战笔记 数组、切片和映射)