介绍
与其他语言不同,在 Go 中,字典(map)是无序集合。在本实验中,我们将学习如何对字典进行排序,以及如何更灵活地使用它们。
核心概念:
- 字典排序
- 交换字典中的键和值
- 字典切片
- 以切片作为值的字典
- 字典的引用类型特性
与其他语言不同,在 Go 中,字典(map)是无序集合。在本实验中,我们将学习如何对字典进行排序,以及如何更灵活地使用它们。
核心概念:
在 ~/project 目录下创建一个 map.go 文件:
touch ~/project/map.go
将以下代码写入 map.go 文件:
package main
import (
"fmt"
)
func main() {
// 声明并初始化一个键为字符串、值为整数的 map
// 该 map 存储学生姓名及其分数
m := map[string]int{
"Alice": 99, // 每个键值对代表一个学生和他们的分数
"Bob": 38,
"Charlie": 84,
}
// 使用 for-range 循环遍历 map
// 'key' 代表学生姓名,'value' 代表分数
for key, value := range m {
fmt.Println(key, value)
}
fmt.Println("\nInsert Data")
// 演示如何向 map 中添加新的键值对
// 语法为:map[key] = value
m["David"] = 25
// 再次遍历以显示更新后的 map 内容
// 注意,每次执行时的顺序可能会有所不同
for key, value := range m {
fmt.Println(key, value)
}
}
运行程序:
go run ~/project/map.go
程序的输出可能如下所示:
Charlie 84
Bob 38
Alice 99
Insert Data
David 25
Charlie 84
Bob 38
Alice 99
输出结果可能会有所不同,因为插入数据的顺序是不固定的。这是 Go map 的一个核心特性——当你遍历它们时,不保证元素的任何特定顺序。
你可以尝试多次运行该程序,并观察插入数据的顺序可能会发生变化。这说明你不能依赖 map 中元素的顺序。
但有时我们需要在插入数据后对字典进行排序。我们该如何实现呢?
由于 map 本身无法排序,我们可以将 map 转换为切片,然后对切片进行排序。
首先,让我们学习如何按键(key)对字典进行排序。
步骤如下:
sort 包对切片进行排序。将以下代码写入 map.go 文件:
package main
import (
"fmt"
"sort"
)
func main() {
// 初始化字典
m1 := map[int]string{
3: "Bob",
1: "Alice",
2: "Charlie",
}
keys := make([]int, 0, len(m1)) // 初始化具有容量的切片。这是一种性能优化——切片最初将分配与 map 大小相等的内存,从而避免重新分配。
for key := range m1 {
// 将键添加到切片中
keys = append(keys, key)
}
// 使用 sort 包对键切片进行排序。`sort.Ints()` 函数专门用于对整数切片进行排序。
sort.Ints(keys)
for _, key := range keys {
// 现在输出是有序的
fmt.Println(key, m1[key])
}
}
运行程序:
go run ~/project/map.go
程序的输出如下:
1 Alice
2 Charlie
3 Bob
通过这种方法,我们实现了基于键的字典排序。键被提取到切片中并进行排序,然后用于从 map 中查找并打印相应的值。
在解释如何按值排序之前,我们先学习如何交换字典中的键和值。
交换键和值意味着互换字典中键和值的位置,如下图所示:

实现代码非常简单。将以下代码写入 map.go 文件:
package main
import "fmt"
func main() {
m := map[string]int{
"Alice": 99,
"Bob": 38,
"Charlie": 84,
}
m2 := map[int]string{}
for key, value := range m {
m2[value] = key
}
fmt.Println(m2)
}
运行程序:
go run ~/project/map.go
程序的输出如下:
map[38:Bob 84:Charlie 99:Alice]
这段代码的核心是从原始字典中提取键和值,然后将它们以互换的角色重新插入到新字典中。这非常简单直接。请注意,由于 map 是无序的,交换后 map 的输出顺序可能会有所不同。
结合按键排序的逻辑和交换键值的逻辑,我们可以实现按值(value)对字典进行排序。
其工作原理是:我们交换键和值,然后将交换后的键(即原始值)作为排序的基础。接着,我们使用排序后的“键”(原始值)在交换后的 map 中查找对应的原始键。
将以下代码写入 map.go 文件:
package main
import (
"fmt"
"sort"
)
func main() {
// 初始化字典
m1 := map[string]int{
"Alice": 99,
"Bob": 38,
"Charlie": 84,
}
// 初始化反转后的字典
m2 := map[int]string{}
for key, value := range m1 {
// 通过交换键值对生成反转字典 m2
m2[value] = key
}
values := make([]int, 0) // 初始化用于排序的切片
for _, value := range m1 {
// 将原始字典的值转换到切片中
values = append(values, value)
}
// 使用 sort 包对值切片进行排序
sort.Ints(values)
for _, value := range values {
// 现在输出是有序的
fmt.Println(m2[value], value)
}
}
运行程序:
go run ~/project/map.go
程序的输出如下:
Bob 38
Charlie 84
Alice 99
现在我们已经根据值对字典进行了排序。我们将值转换为切片并排序,然后使用排序后的值从交换后的 map 中检索并打印对应的原始键。
如果 Go 的版本高于 1.7,我们可以使用 sort.Slice 函数快速按键或值对 map 进行排序。sort.Slice 允许你指定自定义的比较函数。
示例如下:
package main
import (
"fmt"
"sort"
)
func main() {
m1 := map[int]int{
21: 99,
12: 98,
35: 17,
24: 36,
}
type kv struct {
Key int
Value int
}
var s1 []kv
for k, v := range m1 {
s1 = append(s1, kv{k, v})
}
sort.Slice(s1, func(i, j int) bool {
return s1[i].Key < s1[j].Key
})
fmt.Println("Sorted in ascending order by key:")
for _, pair := range s1 {
fmt.Printf("%d, %d\n", pair.Key, pair.Value)
}
}
运行程序:
go run ~/project/map.go
输出如下:
Sorted in ascending order by key:
12, 98
21, 99
24, 36
35, 17
在这个程序中,我们使用结构体 kv 来存储来自 map 的键值对。然后,我们使用 sort.Slice() 函数和一个匿名比较函数对结构体切片进行排序。这个比较函数(func(i, j int) bool)根据结构体的 Key 字段决定排序顺序。
我们还可以通过修改这个比较函数,使用 sort.Slice 按键降序或按值升序对 map 进行排序。这为我们如何排序 map 数据提供了极大的灵活性。
创建一个 map2.go 文件,并修改上一节的代码,使其根据值对 map 进行降序排序。
预期输出:
运行程序:
go run ~/project/map2.go
Sorted in descending order by value:
21, 99
12, 98
24, 36
35, 17
要求:
map2.go 文件应放置在 ~/project 目录下。sort.Slice 函数。你需要修改上一个示例中 sort.Slice 所使用的比较函数。正如我们可以使用数组或切片来存储相关数据一样,我们也可以使用元素为字典的切片。这让我们可以持有 map 数据的集合,这在处理结构化信息时非常有用。
将以下代码写入 map.go 文件:
package main
import "fmt"
func main() {
// 声明一个 map 切片并使用 make 进行初始化
var mapSlice = make([]map[string]string, 3) // 创建一个容量为 3 的切片,其中每个元素都可以是一个 `map[string]string`。
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
fmt.Println("Initialization")
// 为切片的第一个元素赋值
mapSlice[0] = make(map[string]string, 10) // 在第一个索引处创建一个 map。
mapSlice[0]["name"] = "labex"
mapSlice[0]["password"] = "123456"
mapSlice[0]["address"] = "Paris"
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
}
运行程序:
go run ~/project/map.go
程序的输出如下:
index:0 value:map[]
index:1 value:map[]
index:2 value:map[]
Initialization
index:0 value:map[address:Paris name:labex password:123456]
index:1 value:map[]
index:2 value:map[]
代码演示了 map 切片的初始化。最初,切片中的每个元素都是一个空 map。然后我们创建并向第一个元素分配一个 map,并填充数据。这展示了如何管理 map 列表。
我们还可以使用以切片作为值的字典,以便在字典中存储更多数据。这让我们可以将多个值与 map 中的单个键关联起来,从而有效地创建“一对多”的关系。
将以下代码写入 map.go 文件:
package main
import "fmt"
func main() {
var sliceMap = make(map[string][]string, 3) // 声明一个键为字符串、值为字符串切片的 map。3 是容量提示。
key := "labex"
value, ok := sliceMap[key] // 检查键是否存在
if !ok {
value = make([]string, 0, 2) // 如果不存在,初始化一个具有容量的新切片。
}
value = append(value, "Paris", "Shanghai") // 向切片追加值。
sliceMap[key] = value // 将切片设置为 map 中对应键的值。
fmt.Println(sliceMap)
}
运行程序:
go run ~/project/map.go
程序的输出如下:
map[labex:[Paris Shanghai]]
代码首先声明了一个值为切片的 map 类型。在向关联切片添加新项之前,它会检查键是否存在,这展示了处理切片 map 的常见模式。
数组是值类型,因此将它们赋值或传递给函数会创建一个副本。对副本的更改不会影响原始数组。然而,map 是引用类型。这意味着将 map 赋值或传递给函数不会创建完整的副本。相反,map 是通过引用传递的。
这一点非常重要,因为在函数内部进行的更改会影响原始 map 数据。
将以下代码写入 map.go 文件:
package main
import "fmt"
func modifyMap(x map[string]int) {
x["Bob"] = 100 // 修改作为参数传递的 map。
}
func main() {
a := map[string]int{
"Alice": 99,
"Bob": 38,
"Charlie": 84,
}
// 因为 map 是通过引用传递的,modifyMap 中的修改会改变原始字典
modifyMap(a)
fmt.Println(a) // map[Alice:99 Bob:100 Charlie:84]
}
运行程序:
go run ~/project/map.go
程序的输出如下:
map[Alice:99 Bob:100 Charlie:84]
在这个例子中,我们演示了字典的引用传递特性。modifyMap 函数更改了原始 map,因为 a 是对同一底层 map 数据的引用。在将 map 传递给函数时,理解这种行为至关重要。
在本实验中,我们学习了 Go 中 map 的高级用法,包括:
掌握这些概念将使你在实际应用中更有效地使用 Go map。