4. 数组、切片和映射(Go Tutorial)

数组、切片和映射(Go Tutorial)

4.1 数组的内部实现和基础功能

4.1.1 内部实现

在 Go 语言里,数据是一个长度固定的数据类型,用于存储一段具有相同的类型的元素的连续块。

数据存储的类型可以是内置类型,如 int 和 string 等,也可以是自定义类型。

由于数组的每个元素类型相同,又是连续分配,所以可以以固定速度索引数组中的任意数据,速度非常快。

4.1.2 声明和初始化

  • 声明一个数组并初始化为零值
// 声明一个包含 5 个元素的整形数组
var array [5]int
  • 使用数组字面量声明数组
// 声明一个包含5个元素的整形数组并使用具体值初始化每个元素
array := [5]int{10, 20, 30, 40, 50}
// `...` 表示容量由初始化值的数量决定
array := [...]int{10, 20, 30, 40}
  • 声明数据并指定特定元素的值
// 声明一个有 5 个元素的数组,用具体值初始化索引为 1 和 2 的元素,其余元素保存零值
array := [5]int{1: 10, 2:20}

4.1.3 使用数组

  • 访问数组元素
array := [5]int{10, 20, 30, 40, 50}
// 修改数组中索引为 2 的元素的值
array[2] = 35
  • 访问指针数组的元素
// 声明包含 5 个元素的指向整形的数组
array := [5]*int{0: new(int), 1: new(int)}
// 为索引 0 和 1 的元素赋值
*array[0] = 10
*array[1] = 20
  • 把一个指针数组赋值给另一个
var array1 [3]*string
array2 := [3]*string{new(string), new(string), new(string)}
*array2[0] = "a"
*array2[1] = "b"
*array3[2] = "c"
array1 = array2

数组变量的类型包括长度和每个元素的类型,这有这两部分都相同的数组,才能互相赋值,否则编译器会报错

4.1.4 多维数组

  • 声明二维数组
// 声明一个二维数组,两个维度分别为存储 4 个元素和 2 个元素
var array [4][2]int
// 使用数组字面量来声明并初始化一个二维整形数组
array := [4][2]int{{10, 11}, {12, 13}, {14, 15}, {16, 17}}
// 声明并初始化外层数组中索引为 1 和 3 的元素
array := [4][2]int{1: {20, 21}, 3: {30, 31}}
// 声明并初始化外层数组和内层数组的单个元素
array := [4][2]int{1: (0: 20), 3: (1: 31)}
  • 访问二维数组
var array [2][2]int
array[0][0] = 10
array[0][1] = 20
array[1][0] = 30
arrat[1][1] = 40

4.1.5 在函数之间传递数组

在函数之间传递数组是一个开销很大的操作。因为在函数之间传递数组时,总是以值的方式传递的。

  • 使用指针在函数间传递大数组
var array [le6]int
foo(&array)
func foo(array *[le6]int) {
  // ....
}

虽然将数组的地址传给函数,不会带来很大的内存开销,但是由于传递的是指针,如果改变指针指向的值,会改变共享的内存。

4.2 切片的内部实现和基础功能

4.2.1 内部实现

切片是一种便于使用和管理的数据集合,其是围绕动态数组的概念构建的,可以按需自动增长和缩小。

切片是一个很小的对象,对底层数组进行了抽象,并提供相关的操作方法。

切片有 3 个字段的数据节后,这些数据结构包含 Go 语言需要操作底层数组的元数据。

这 3 个字段分别是指向底层数组的指针、切片的长度、切片的容量。

4.2.2 创建和初始化

  1. make 和 切片字面量

    • 使用长度声明一个字符串切片
// 创建一个字符串切片
// 其长度和容量都是 5 个元素
slice := make([]string, 5)

如果只指定长度,那么容量和长度相等

  • 使用长度和容量声明整形切片
// 创建一个整形切片
// 其长度为 4,容量为 7
slice := make([]int, 4, 7)

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

  • 通过字面量来声明切片
slice := []string{"red", "blue", "yellow"}
slice := []int{10, 20, 30}
  • 使用切片字面量,可以设置初始化长度和容量
// 创建字符串切片
// 使用空字符串初始化第 100 个元素
slice := []string{99: ""}
  • 声明数组和切片的不同
// 声明数组
array := [3]int{10, 20, 30}
// 声明切片
slice := []int{10, 20, 30}

如果在 [] 运算符里指定了一个值,那么创建的就是数组而不是切片

  1. nil 和空切片

    • 创建 nil 切片
// 创建 nil 整形切片
var slice []int

需要描述一个不存在的切片时,nil 切片会很好用

  • 创建空切片
slice := make([]int, 0)
slice := []int{}

在表示空集合时,空切片很有用

4.2.3 使用切片

4.2.3.1 赋值和切片

对切片元素赋值与数组元素的赋值一样

  • 使用切片字面量来声明切片
slice := []int{10, 20, 30}
slice[1] = 21
  • 使用切片创建切片
slice := []int{10, 20, 30}
newSlice := slice[1:2]

上面两个切片共享一段底层数组,但通过不同的切片会看到底层数组的不同部分

  • 计算长度和容量

对于底层数组容量是 k 的切片 slice[i:j] 来说

长度:j - i

容量:k - i

  • 修改切片内容可能导致的结果
slice := []int{10, 20, 30}
newSlice := slice[1:2]
newSlice[0] = 21
// output:21
fmt.Pringln(slice[1])

4.2.3.2 切片增长

相对数组而言,使用切片的一个好处是,可以按需增长切片的容量。

Go 语言内置的 append 函数会处理增加长度时的所有操作细节。append 调用返回时,会返回一个包含修改结果的新切片。

函数 append 总是会增加新切片的长度,而容量有可能会改变,也可能不会改变,这取决与被操作的切片是否有可用的容量。

  • 使用 append 向切片增加元素
slice := []int{10, 20, 30}
newSlice := slice[0 : 1]
newSlice = append(newSlice, 21)
// [10 21]
fmt.Println(newSlice)
// [10 21 30]
fmt.Println(slice)

因为 newSlice 在底层数组还有容量可以,所以和 slice 共用的是同一个底层数组,对 newSlice 的修改就是修改共用的底层数组

  • 使用 append 同时增加长度和容量
slice := []int{10, 20, 30, 40}
newSlice := append(slice, 50)
newSlice[0] = 11
// [10 20 30 40]
fmt.Println(slice)
// [11 20 30 40 50]
fmt.Println(newSlice)

如果切片的底层数组没有足够的可以容量,append 函数会创建一个新的底层数组,将被引用的现有的值复制到新数组里,再追加新的值。

在上面的例子中,newSlice 与 slice 不再共用同一个底层数组。

append 函数会智能的处理底层数组的容量增长:当切片的容量小于 1000 个元素时,总是成倍的增长容量。一旦元素个数超过 1000,容量的增长因子会设为 1.25,也就是每次增长 25% 的容量。

4.2.3.3 创建切片的 3 个索引

在创建切片时,还可以使用第三个索引选项,第三个索引选项可以用来控制新切片的容量,其目的并不是要增加容量,而是要限制容量,这为底层数组提供了一定的保护,可以更好的控制追加操作。

  • 使用 3 个索引创建切片
source := []string{"apple", "orange", "banana", "grape", "plum"}
slice := source[2 : 3 : 4]

对于 slice[i : j : k],其长度为 j - i,容量为 k - i。

当设置容量大于已有容量时编译器会报错

  • 设置长度和容量一致
source := []string{"apple", "orange", "banana", "grape", "plum"}
slice := source[1:3:3]
slice = append(slice, "kiwi", "hello")
// [orange banana kiwi hello]
fmt.Println(slice)

如果在创建切片时设置切片的容量和长度一样,就可以强制让新切片的第一个 append 函数操作创建新的底层数组,与原有的底层数组分离。

  • 将一个切片追加到另一个切片
s1 := []int{1, 2}
s2 := []int{2, 4}
// [1 2 2 4]
fmt.Printf("%v\n", append(s1, s2...))

4.2.3.4 迭代切片

Go 语言里可以使用关键字 range,它可以配合关键字 for 来迭代切片里的元素

  • 使用 range 迭代切片
slice := []int{10, 20, 30}
for _, value := range slice {
  fmt.Printf("value: %d\n", value)
}
for index, value := range slice {
  fmt.Printf("index: %d, value: %d\n", index, value)
}
  • 使用传统的 for 循环迭代切片
slice := []int{10, 20}
for index := 2; index < len(slice); index++ {
  fmt.Printf("index: %d, value: %d\n", index, slice[index])
}

内置函数 len 和 cap,可以用于处理数组、切片和通道。对于切片,len 返回长度,cap 返回容量

4.2.4 多维切片

  • 声明多维切片
slice := [][]int{{10}, {20, 30}}
slice[0] = append(slice[0], 20)
// [[10 20] [20 30]]
fmt.Println(slice)

4.2.5 在函数间传递切片

由于与切片关联的数据包含在底层数组里,不属于切片本身,所以将切片复制到任意函数的时候,对底层数组大小都不会有影响。复制时只会赋值切片本身,所以在函数间传递会非常快速、简单。

package main

import (
    "fmt"
)

func test(slice []int)  {
    var newSlice []int
    newSlice = slice
    newSlice = slice[0:len(slice):len(slice)]
    newSlice = append(newSlice, 40)
    newSlice[0] = 10
    fmt.Println(newSlice)
}

func main() {
    slice := []int{1, 2, 3, 4, 5}
    test(slice[0:3])
    fmt.Println(slice)
    // [10 2 3 40] 
    // [1 2 3 4 5]
}

4.3 映射的内部实现和基础功能

映射是一种存储一系列无序键值对的数据结构

4.3.1 内部实现

只需记住一件事:映射是一个存储键值对的无序集合

4.3.2 创建和初始化

  • 使用 make 声明映射
dict := make(map[string]int)
dict := map[string]string{"name": "li", "gender": "male"}
  • 声明一个存储字符串切片的映射
// 创建一个映射,使用字符串切片作为值
dict := map[int][]string{}

4.3.3 使用映射

func main() {
    // 创建一个空映射并赋值
    colors := map[string]string{}
    colors["red"] = "RED"
    fmt.Println(colors)
    // 对 nil 映射赋值时,会报错
    var colors2 map[string]string
    colors2["red"] = "RED"
    fmt.Println(colors2)
}

从映射取值时有两个选择。

  • 第一个选择是,可以同时获得值,以及一个表示这个是否存在的标志
value, exists := colors["blue"]
// 这个键存在吗
if exists {
  fmt.Println(value)
}
  • 另一个选择是,只返回键对应的值,然后判断这个值是否是零值来判断键是否存在
value := colors["blue"]
if value != "" {
  fmt.Println(value)
}

使用 range 关键字迭代映射

func main() {
    colors := map[string]string{"name": "lilei", "gender": "male", "country": "china"}
    for key, value := range colors {
        fmt.Printf("%s is %s\n", key, value)
    }
}

从映射中删除一项

func main() {
    colors := map[string]string{"name": "liLei", "gender": "male", "country": "china"}
    delete(colors, "name")
    for key, value := range colors {
        fmt.Printf("%s is %s\n", key, value)
    }
}

4.3.4 在函数间传递映射

将映射传递给函数成本很小,并且不会赋值底层的数据结构

package main

import (
    "fmt"
)

func removeColors(colors map[string]string, key string) map[string]string {
    delete(colors, key)
    return colors
}

func iterator(colors map[string]string)  {
    for key, value := range colors {
        fmt.Printf("%s is %s\n", key, value)
    }
}

func main() {
    colors := map[string]string{"name": "liLei", "gender": "male", "country": "china"}
    iterator(removeColors(colors, "name"))
    iterator(colors)
    // output is:
    // gender is male
    // country is china
    // gender is male
    // country is china
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值