Press "Enter" to skip to content

Go 数据类型篇(九):字典使用入门与字典排序实现

字典声明和初始化

有过 Redis 使用经验的同学应该很熟悉,所谓字典,其实就是存储键值对映射关系的集合,只不过对于强类型的 Go 语言来说,需要在声明时指定键和值的类型,此外,和 Redis 一样,Go 字典也是个无序集合,底层不会按照元素添加顺序维护元素的存储顺序。

简单示例

下面我们通过一个简单的示例来看看如何在 Go 语言中使用字典这种数据类型:

var testMap map[string]int
testMap = map[string]int{
  "one": 1,
  "two": 2,
  "three": 3,
}

k := "two"
v, ok := testMap[k]
if ok {
  fmt.Printf("The element of key %q: %d\n", k, v)
} else {
  fmt.Println("Not found!")
}

上面这个简单的例子基本上已经覆盖了 map 的主要用法,下面对其中的关键点进行细述。

字典声明

字典的声明基本上没有多余的元素,比如:

var testMap map[string]int

其中,testMap 是声明的字典变量名,string 是键的类型,int 则是其中所存放的值类型。

字典初始化

我们可以通过先声明再初始化的方式进行初始化,就像上面示例代码做的那样,也可以通过 := 将声明和初始化合并为一条语句:

testMap := map[string]int{
  "one": 1,
  "two": 2,
  "three": 3,
}

前面我们提到 Go 字典是个无序集合,所以如果我们通过 fmt.Println(testMap) 打印 testMap 的值,得到的可能是下面这样的结果:

map[one:1 three:3 two:2]

此外,还可以像切片那样,通过 Go 语言内置的函数 make() 来初始化一个新字典:

var testMap = make(map[string]int)

通过这种方式初始化后,可以往字典中添加键值对(前面那种声明方式不能这么操作,否则编译期间会抛出 panic):

testMap["one"] = 1
testMap["two"] = 2
testMap["three"] = 3

还可以通过 make 函数的第二个参数选择是否在创建时指定该字典的初始存储容量(超出会自动扩容):

testMap = make(map[string]int, 100)

使用入门

元素赋值

赋值过程非常简单明了,只需为给定键赋值即可:

testMap["four"] = 4

需要注意的是,字典初始化之后才能进行赋值操作,如果仅仅是声明,此时 testMap 的值为 nil,在 nil 上进行操作编译期间会报 panic(运行时恐慌),导致编译不通过。

查找元素

在 Go 语言中,字典的查找功能设计得比较精巧,要从字典中查找一个特定的键对应的值,可以通过下面的代码来实现:

value, ok := testMap["one"] 
if ok { // 找到了
  // 处理找到的value 
}

从字典中查找指定键时,会返回两个值,第一个是真正返回的键值,第二个是是否找到的标识,判断是否在字典中成功找到指定的键,不需要检查取到的值是否为 nil,只需查看第二个返回值 ok,这是一个布尔值,如果查找成功,返回 true,否则返回 false,配合 := 操作符,让你的代码没有多余成分,看起来非常清晰易懂。

Go 语言中的字典和 Redis 一样,底层也是通过哈希表实现的,添加键值对到字典时,实际是将键转化为哈希值进行存储,在查找时,也是先将键转化为哈希值去哈希表中查询,从而提高性能。

但是哈希表存在哈希冲突问题,即不同的键可能会计算出同样的哈希值,这个时候 Go 底层还会判断原始键的值是否相等。也正因如此,我们在声明字典的键类型时,要求数据类型必须是支持通过 ==!= 进行判等操作的类型,比如数字类型、字符串类型、数组类型、结构体类型等,不过为了提高字典查询性能,类型长度越短约好,通常,我们会将其设置为整型或者长度较短的字符串类型。

删除元素

Go 语言提供了一个内置函数 delete(),用于删除容器内的元素,我们可以通过这个函数来实现字典元素的删除:

delete(testMap, "four")

上面的代码将会从 testMap 中删除键为「four」的键值对。如果「four」这个键不存在或者字典尚未初始化,这个调用也不会有什么副作用。

遍历字典

我们可以像遍历数组那样对字典类型数据进行遍历:

testMap := map[string]int{
    "one": 1,
    "two": 2,
    "three": 3,
}

for key, value := range testMap {
    fmt.Println(key, value)
}

由于字典是无序的,所以上述代码输出结果如下:

three 3
one 1
two 2

当然,我们还可以借助匿名变量只获取字典的值:

for _, value := range testMap {
    fmt.Println(value)
}

或者像这样只获取字典的键名:

for key := range testMap {
    fmt.Println(key)
}

键值对调

所谓键值对调,指的是交换字典的键和值,在一些编程语言中,内置了相应的函数,比如 PHP 的 array_flip 函数。在 Go 语言中,我们需要手动编写代码来实现,如果我们要对调 testMap 字典的键值,可以这么做:

invMap := make(map[int] string, 3)

for k, v := range testMap {
    invMap[v] = k
}

for k, v := range invMap {
    fmt.Println(k, v)
}

上述代码的打印结果是:

3 three
1 one
2 two

字典排序

我们已经知道 Go 语言的字典是一个无序集合,如果你想要对字典进行排序,可以通过分别为字典的键和值创建切片,然后通过对切片进行排序来实现。

按照键进行排序

如果要对字典按照键进行排序,可以这么做:

keys := make([]string, 0)
for k, _ := range testMap {
    keys = append(keys, k)
}

sort.Strings(keys)  // 对键进行排序

fmt.Println("Sorted map by key:")
for _, k := range keys {
    fmt.Println(k, testMap[k])
}

上述代码打印结果是:

Sorted map by key:
one 1
three 3
two 2

该结果是按照键名在字母表中的排序进行升序排序的结果。

按照值进行排序

如果要对字典按照值进行排序,可以这么做:

values := make([]int, 0)
for _, v := range testMap {
    values = append(values, v)
}

sort.Ints(values)   // 对值进行排序

fmt.Println("Sorted map by value:")
for _, v := range values  {
    fmt.Println(invMap[v], v)
}

这里我们借助了之前创建的 invMap 通过字典的值反查对应的键,上述代码打印结果如下:

Sorted map by value:
one 1
two 2
three 3

该结果是按照键值对应数字大小进行升序排序的结果。

另外,你可能已经注意到我们在对切片进行排序时,使用了 Go 语言内置的 sort 包,这个包提供了一系列对切片和用户自定义集合进行排序的函数。

6 Comments

  1. Hesunfly
    Hesunfly 2021年3月25日

    感觉可以把字典看成php的非索引数组,哈哈

  2. dh-dh
    dh-dh 2023年3月1日

    php的数组是做了多少封装啊,用起来这么丝滑

    • 学院君
      学院君 2023年3月1日

      php:什么 array slice list set map hash 在我这里一个数组全搞定
      php 数组是无敌的存在,离开了就知道它的香 哈哈

发表回复