learn-tech/专栏/22讲通关Go语言-完/04集合类型:如何正确使用array、slice和map?.md
2024-10-15 23:13:09 +08:00

423 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
04 集合类型:如何正确使用 array、slice 和 map
上节课的思考题是练习使用 for 循环中的 continue通过上节课的学习你已经了解 continue 是跳出本次循环的意思,现在我就以计算 100 以内的偶数之和为例,演示 continue 的用法:
sum := 0
for i:=1; i<100; i++{
if i%2!=0 {
continue
}
sum+=i
}
fmt.Println("the sum is",sum)
这个示例的关键在于如果 i 不是偶数就会用 continue 跳出本次循环继续下个循环如果是偶数则继续执行 sum+=i然后继续循环这样就达到了只计算 100 以内偶数之和的目的
下面我们开始本节课的学习我将介绍 Go 语言的集合类型
在实际需求中我们会有很多同一类型的元素放在一起的场景这就是集合例如 100 个数字10 个字符串等 Go 语言中数组array)、切片slice)、映射map这些都是集合类型用于存放同一类元素虽然都是集合但用处又不太一样这节课我就为你详细地介绍
Array数组
数组存放的是固定长度相同类型的数据而且这些存放的元素是连续的所存放的数据类型没有限制可以是整型字符串甚至自定义
数组声明
要声明一个数组非常简单语法和第二课时介绍的声明基础类型是一样的
在下面的代码示例中我声明了一个字符串数组长度是 5所以其类型定义为 [5]string其中大括号中的元素用于初始化数组此外在类型名前加 [] 中括号并设置好长度就可以通过它来推测数组的类型
注意[5]string [4]string 不是同一种类型也就是说长度也是数组类型的一部分
ch04/main.go
array:=[5]string{"a","b","c","d","e"}
数组在内存中都是连续存放的下面通过一幅图片形象地展示数组在内存中如何存放
可以看到数组的每个元素都是连续存放的每一个元素都有一个下标Index)。下标从 0 开始比如第一个元素 a 对应的下标是 0第二个元素 b 对应的下标是 1以此类推通过 array+[下标] 的方式我们可以快速地定位元素
如下面代码所示运行它可以看到输出打印的结果是 c也就是数组 array 的第三个元素
ch04/main.go
func main() {
array:=[5]string{"a","b","c","d","e"}
fmt.Println(array[2])
}
在定义数组的时候数组的长度可以省略这个时候 Go 语言会自动根据大括号 {} 中元素的个数推导出长度所以以上示例也可以像下面这样声明
array:=[...]string{"a","b","c","d","e"}
以上省略数组长度的声明只适用于所有元素都被初始化的数组如果是只针对特定索引元素初始化的情况就不适合了如下示例
array1:=[5]string{1:"b",3:"d"}
示例中的1:b”,3:d”」的意思表示初始化索引 1 的值为 b初始化索引 3 的值为 d整个数组的长度为 5如果我省略长度 5那么整个数组的长度只有 4显然不符合我们定义数组的初衷
此外没有初始化的索引其默认值都是数组类型的零值也就是 string 类型的零值 “” 空字符串
除了使用 [] 操作符根据索引快速定位数组的元素外还可以通过 for 循环打印所有的数组元素如下面的代码所示
ch04/main.go
for i:=0;i<5;i++{
fmt.Printf("数组索引:%d,对应值:%s\n", i, array[i])
}
数组循环
使用传统的 for 循环遍历数组输出对应的索引和对应的值这种方式很烦琐一般不使用大部分情况下我们使用的是 for range 这种 Go 语言的新型循环如下面的代码所示
for i,v:=range array{
fmt.Printf("数组索引:%d,对应值:%s\n", i, v)
}
这种方式和传统 for 循环的结果是一样的对于数组range 表达式返回两个结果
第一个是数组的索引
第二个是数组的值
在上面的示例中把返回的两个结果分别赋值给 i v 这两个变量就可以使用它们了
相比传统的 for 循环for range 要更简洁如果返回的值用不到可以使用 _ 下划线丢弃如下面的代码所示
for _,v:=range array{
fmt.Printf("对应值:%s\n", v)
}
数组的索引通过 _ 就被丢弃了只使用数组的值 v 即可
Slice切片
切片和数组类似可以把它理解为动态数组切片是基于数组实现的它的底层就是一个数组对数组任意分隔就可以得到一个切片现在我们通过一个例子来更好地理解它同样还是基于上述例子的 array
基于数组生成切片
下面代码中的 array[2:5] 就是获取一个切片的操作它包含从数组 array 的索引 2 开始到索引 5 结束的元素
array:=[5]string{"a","b","c","d","e"}
slice:=array[2:5]
fmt.Println(slice)
注意这里是包含索引 2但是不包含索引 5 的元素即在 : 右边的数字不会被包含
ch04/main.go
//基于数组生成切片包含索引start但是不包含索引end
slice:=array[start:end]
所以 array[2:5] 获取到的是 cde 这三个元素然后这三个元素作为一个切片赋值给变量 slice
切片和数组一样也可以通过索引定位元素这里以新获取的 slice 切片为例slice[0] 的值为 cslice[1] 的值为 d
有没有发现在数组 array 元素 c 的索引其实是 2但是对数组切片后在新生成的切片 slice 它的索引是 0这就是切片虽然切片底层用的也是 array 数组但是经过切片后切片的索引范围改变了
通过下图可以看出切片是一个具备三个字段的数据结构分别是指向数组的指针 data长度 len 和容量 cap
这里有一些小技巧切片表达式 array[start:end] 中的 start end 索引都是可以省略的如果省略 start那么 start 的值默认为 0如果省略 end那么 end 的默认值为数组的长度如下面的示例
array[:4] 等价于 array[0:4]。
array[1:] 等价于 array[1:5]。
array[:] 等价于 array[0:5]。
切片修改
切片的值也可以被修改这里也同时可以证明切片的底层是数组
对切片相应的索引元素赋值就是修改在下面的代码中把切片 slice 索引 1 的值修改为 f然后打印输出数组 array
slice:=array[2:5]
slice[1] ="f"
fmt.Println(array)
可以看到如下结果
[a b c f e]
数组对应的值已经被修改为 f所以这也证明了基于数组的切片使用的底层数组还是原来的数组一旦修改切片的元素值那么底层数组对应的值也会被修改
切片声明
除了可以从一个数组得到切片外还可以声明切片比较简单的是使用 make 函数
下面的代码是声明了一个元素类型为 string 的切片长度是 4make 函数还可以传入一个容量参数
slice1:=make([]string,4)
在下面的例子中指定了新创建的切片 []string 容量为 8
slice1:=make([]string,4,8)
这里需要注意的是切片的容量不能比切片的长度小
切片的长度你已经知道了就是切片内元素的个数那么容量是什么呢其实就是切片的空间
上面的示例说明Go 语言在内存上划分了一块容量为 8 的内容空间容量为 8但是只有 4 个内存空间才有元素长度为 4其他的内存空间处于空闲状态当通过 append 函数往切片中追加元素的时候会追加到空闲的内存上当切片的长度要超过容量的时候会进行扩容
切片不仅可以通过 make 函数声明也可以通过字面量的方式声明和初始化如下所示
slice1:=[]string{"a","b","c","d","e"}
fmt.Println(len(slice1),cap(slice1))
可以注意到切片和数组的字面量初始化方式差别就是中括号 [] 里的长度此外通过字面量初始化的切片长度和容量相同
Append
我们可以通过内置的 append 函数对一个切片追加元素返回新切片如下面的代码所示
//追加一个元素
slice2:=append(slice1,"f")
//多加多个元素
slice2:=append(slice1,"f","g")
//追加另一个切片
slice2:=append(slice1,slice...)
append 函数可以有以上三种操作你可以根据自己的实际需求进行选择append 会自动处理切片容量不足需要扩容的问题
小技巧在创建新切片的时候最好要让新切片的长度和容量一样这样在追加操作的时候就会生成新的底层数组从而和原有数组分离就不会因为共用底层数组导致修改内容的时候影响多个切片
切片元素循环
切片的循环和数组一模一样常用的也是 for range 方式这里就不再进行举例当作练习题留给你
Go 语言开发中切片是使用最多的尤其是作为函数的参数时相比数组通常会优先选择切片因为它高效内存占用小
Map映射
Go 语言中map 是一个无序的 K-V 键值对集合结构为 map[K]V其中 K 对应 KeyV 对应 Valuemap 中所有的 Key 必须具有相同的类型Value 也同样 Key Value 的类型可以不同此外Key 的类型必须支持 == 比较运算符这样才可以判断它是否存在并保证 Key 的唯一
Map 声明初始化
创建一个 map 可以通过内置的 make 函数如下面的代码所示
nameAgeMap:=make(map[string]int)
它的 Key 类型为 stringValue 类型为 int有了创建好的 map 变量就可以对它进行操作了
在下面的示例中我添加了一个键值对Key 为飞雪无情Value 20如果 Key 已经存在则更新 Key 对应的 Value
nameAgeMap["飞雪无情"] = 20
除了可以通过 make 函数创建 map 还可以通过字面量的方式同样是上面的示例我们用字面量的方式做如下操作
nameAgeMap:=map[string]int{"飞雪无情":20}
在创建 map 的同时添加键值对如果不想添加键值对使用空大括号 {} 即可要注意的是大括号一定不能省略
Map 获取和删除
map 的操作和切片数组差不多都是通过 [] 操作符只不过数组切片的 [] 中是索引 map [] 中是 Key如下面的代码所示
//添加键值对或者更新对应 Key Value
nameAgeMap["飞雪无情"] = 20
//获取指定 Key 对应的 Value
age:=nameAgeMap["飞雪无情"]
Go 语言的 map 可以获取不存在的 K-V 键值对如果 Key 不存在返回的 Value 是该类型的零值比如 int 的零值就是 0所以很多时候我们需要先判断 map 中的 Key 是否存在
map [] 操作符可以返回两个值
第一个值是对应的 Value
第二个值标记该 Key 是否存在如果存在它的值为 true
我们通过下面的代码进行演示
ch04/main.go
nameAgeMap:=make(map[string]int)
nameAgeMap["飞雪无情"] = 20
age,ok:=nameAgeMap["飞雪无情1"]
if ok {
fmt.Println(age)
}
在示例中age 是返回的 Valueok 用来标记该 Key 是否存在如果存在则打印 age
如果要删除 map 中的键值对使用内置的 delete 函数即可比如要删除 nameAgeMap Key 为飞雪无情的键值对我们用下面的代码进行演示
delete(nameAgeMap,"飞雪无情")
delete 有两个参数第一个参数是 map第二个参数是要删除键值对的 Key
遍历 Map
map 是一个键值对集合它同样可以被遍历 Go 语言中map 的遍历使用 for range 循环
对于 mapfor range 返回两个值
第一个是 map Key
第二个是 map Value
我们用下面的代码进行演示
ch04/main.go
//测试 for range
nameAgeMap["飞雪无情"] = 20
nameAgeMap["飞雪无情1"] = 21
nameAgeMap["飞雪无情2"] = 22
for k,v:=range nameAgeMap{
fmt.Println("Key is",k,",Value is",v)
}
需要注意的是 map 的遍历是无序的也就是说你每次遍历键值对的顺序可能会不一样如果想按顺序遍历可以先获取所有的 Key并对 Key 排序然后根据排序好的 Key 获取对应的 Value这里我不再进行演示你可以当作练习题
小技巧for range map 的时候也可以使用一个值返回使用一个返回值的时候这个返回值默认是 map Key
Map 的大小
和数组切片不一样map 是没有容量的它只有长度也就是 map 的大小键值对的个数)。要获取 map 的大小使用内置的 len 函数即可如下代码所示
fmt.Println(len(nameAgeMap))
String []byte
字符串 string 也是一个不可变的字节序列所以可以直接转为字节切片 []byte如下面的代码所示
ch04/main.go
s:="Hello飞雪无情"
bs:=[]byte(s)
string 不止可以直接转为 []byte还可以使用 [] 操作符获取指定索引的字节值如以下示例
ch04/main.go
s:="Hello飞雪无情"
bs:=[]byte(s)
fmt.Println(bs)
fmt.Println(s[0],s[1],s[15])
你可能会有疑惑在这个示例中字符串 s 里的字母和中文加起来不是 9 个字符吗怎么可以使用 s[15] 超过 9 的索引呢其实恰恰就是因为字符串是字节序列每一个索引对应的是一个字节而在 UTF8 编码下一个汉字对应三个字节所以字符串 s 的长度其实是 17
运行下面的代码就可以看到打印的结果是 17
fmt.Println(len(s))
如果你想把一个汉字当成一个长度计算可以使用 utf8.RuneCountInString 函数运行下面的代码可以看到打印结果是 9也就是 9 unicodeutf8字符和我们看到的字符的个数一致
fmt.Println(utf8.RuneCountInString(s))
而使用 for range 对字符串进行循环时也恰好是按照 unicode 字符进行循环的所以对于字符串 s 来说循环了 9
在下面示例的代码中i 是索引r unicode 字符对应的 unicode 码点这也说明了 for range 循环在处理字符串的时候会自动地隐式解码 unicode 字符串
ch04/main.go
for i,r:=range s{
fmt.Println(i,r)
}
总结
这节课到这里就要结束了在这节课里我讲解了数组切片和映射的声明和使用有了这些集合类型你就可以把你需要的某一类数据放到集合类型中了比如获取用户列表商品列表等
数组切片还可以分为二维和多维比如二维字节切片就是 [][]byte三维就是 [][][]byte因为不常用所以本节课中没有详细介绍你可以结合我讲的一维 []byte 切片自己尝试练习这也是本节课要给你留的思考题创建一个二维数组并使用它
此外如果 map Key 的类型是整型并且集合中的元素比较少应该尽量选择切片因为效率更高在实际的项目开发中数组并不常用尤其是在函数间作为参数传递的时候用得最多的是切片它更灵活并且内存占用少