Go 语言类型 映射

基本概念

映射(Map)是用于储存一系列无序键值对的数据结构。映射类型适合用于存储和检索关联数据,类似其他编程语言中的哈希表或字典。

在 Go 语言映射中,键(key)和值(value)是组成映射的基本元素,每个键都关联到一个特定值:

  • :键用来在映射中唯一标识值。键类型必须是可比较类型,包括所有基本类型以及数组和结构体,不包括引用类型。
  • :值是映射中与键相关联的数据。在同一个映射中,所有值类型都必须相同,值类型可以是任意类型,包括映射或结构体。

映射提供一种直观方式来表达数据之间的联系,底层实现是一个散列表,属于引用类型。

创建映射

映射在使用前必须初始化,为其分配内存空间,才能存储键值对。

声明和初始化

以下是声明和初始化映射方法:

package main

import "fmt"

func main() {
	// 声明一个 nil 映射,不能直接使用
	// 切片不能作为键,但数组可以
	var a map[[3]int]int

	// 声明并使用字面量初始化映射
	var b = map[string]int{"a": 1, "b": 2}

	// 短变量声明,采用多行赋值形式
	// 如果 } 另起一行,最后一个元素后必须加逗号
	c := map[string]int{
		"c": 3,
		"d": 4,
	}

	// 使用 make 函数创建并初始化空映射
	// 容量参数可选,预设有助于提高性能
	d := make(map[string]int, 100)

	// 输出:map[] map[a:1 b:2] map[c:3 d:4] map[]
	fmt.Println(a, b, c, d)
}

nil 映射

nil 切片不同,nil 映射由于没分配内存,不能直接用于储存键值对。尝试添加键值对会引发运行时错误,必须先通过 make 函数初始化:

package main

import "fmt"

func main() {
	// 声明一个 nil 映射
	var m map[string]int

	// 可以读取 nil 映射
	value := m["k"]
	fmt.Println("读取不存在的键值:", value) // 输出:0

	// 尝试添加键值对会导致运行时错误
	m["k"] = 1 // 报错:assignment to entry in nil map

	// 使用 make 函数初始化
	m = make(map[string]int)
	m["k"] = 1
	fmt.Println("添加键值后:", m) // 输出:map[k:1]
}

nil 映射不占用内存,可以用于延迟初始化或作为函数返回值。

映射操作

只要映射已初始化,映射操作基本是安全的。

读取键值

映射中可以通过键来获取元素值,此外还会获取到键存在标志。如果键值对不存在,存在标志为 false 并返回值类型零值:

package main

import "fmt"

func main() {
	m := map[string]int{"Alice": 68000, "Bob": 72000}

	// 对返回分别赋值
	salary, exists := m["Charlie"]
	if exists {
		fmt.Println(salary)
	} else {
		fmt.Println(salary, exists) // exists 值为 false,salary 为 0
	}
}

键存在标志是通过比较运算得到,所以键才必须是可比较类型。但和切片一样,映射之间不可比较,只能同 nil 做比较。

修改键值

修改映射键值对通过赋值来实现。如果键不存在,会新增键值对记录;如果键存在,赋值操作会更新该键值:

package main

import "fmt"

func main() {
	m := map[string]int{"Alice": 68000, "Bob": 72000}
	fmt.Println("初始工资:", m)

	// 添加新记录
	m["Charlie"] = 90000
	fmt.Println("添加操作后:", m)

	// 更新记录
	m["Alice"] = 71000
	fmt.Println("更新操作后:", m)
}

遍历映射

可以使用 forrange 来遍历映射中所有键值对:

package main

import "fmt"

func main() {
	m := map[string]int{"Alice": 101, "Bob": 102, "Charlie": 104, "David": 103}

	// 使用 len 获取映射中键值对数量
	fmt.Println("员工数量:", len(m))

	// 使用 for 和 range 遍历映射,每次迭代输出顺序可能不同
	for k, v := range m {
		fmt.Printf("%s 在部门编号:%d\n", k, v)
	}
}

由于映射内元素排序不固定,因此不能对映射元素取址。

删除键值

Go 语言提供内置 delete 函数,用于删除映射中的键值对。 delete 函数没有返回值,如果键值对存在则删除;如果不存在则忽略,不会报错:

package main

import "fmt"

func main() {
	m := map[string]int{"Alice": 7000, "Bob": 8000, "Charlie": 9000}
	fmt.Println("初始映射:", m)

	// 删除存在的键
	delete(m, "Bob")
	fmt.Println("第一次删除:", m)

	// 尝试删除不存在的键
	delete(m, "David")
	fmt.Println("第二次删除:", m) // 输出没有变化
}

复制映射

映射是引用类型,所以在函数中对映射修改,会影响所有被传递映射的引用。可以在函数内部创建映射副本,修改并返回副本。复制映射需要创建一个空映射,遍历原始映射键值对来填充新映射:

package main

import "fmt"

func modify(m map[string]int) map[string]int {
	// 创建映射副本
	n := make(map[string]int)
	for k, v := range m {
		n[k] = v
	}

	n["c"] = 3

	return n
}

func main() {
	m := map[string]int{"a": 1, "b": 2}
	n := modify(m)

	fmt.Println("原始映射:", m)
	fmt.Println("新映射:", n)
}

注意,当映射值是复杂数据结构(如切片、其他映射或包含指针的结构体)时,仅复制键值对复制的可能是指针,不是真正的深度复制。除了手动实现更深层次复制逻辑,也可以借助第三方库 copierdeepcopy,或者通过 encoding/gob 包序列化和反序列化对象到内存中来实现深度复制。

模拟集合

Go 语言没有集合类型,可利用映射键特性来模拟集合去重功能:

package main

import "fmt"

func main() {
	// 原始数组
	a := [...]int{1, 2, 3, 2, 1}
	// 建立一个映射模拟集合
	m := map[int]bool{}
	// 创建一个切片来保存结果
	s := []int{}

	// 利用键的唯一性填充映射
	for _, v := range a {
		m[v] = true
	}

	// 通过查询键值来判断状态
	fmt.Println(m[1])  // 值已存入,输出:true
	fmt.Println(m[10]) // 没在映射,输出:false

	// 将键存入切片
	for k := range m {
		s = append(s, k)
	}

	fmt.Println(s) // 输出: [1 2 3]
}

并发映射

映射类型在并发情况下允许同时读,但同时写会导致运行时错误:

package main

func main() {
	m := make(map[int]int)

	// 不停写入
	go func() {
		for {
			m[0] = 0
		}
	}()

	// 不停读取
	go func() {
		for {
			_ = m[1]
		}
	}()

	// 无限循环,让并发程序在后台执行
	for {
	}
}

上面代码会报错:concurrent map read and map write,指出两个并发函数读写映射时发生竟态问题。

除了加锁外,可以使用 sync.Map 替代映射。sync.Map 是一个支持并发安全的映射类型,使用上与原生映射不太相同,需要调用专属方法:

package main

import (
	"fmt"
	"sync"
)

func main() {
	// 不需要初始化,直接声明后使用
	var m sync.Map

	// 储存键值对,以 interface{} 类型保存值
	m.Store("a", 1)
	m.Store("b", 2)
	// 也用于更新键值对
	m.Store("b", 20)

	// 获取键值对
	v, ok := m.Load("b")
	if ok {
		fmt.Println(v) // 输出:20
	}
	fmt.Println(m.Load("c")) // 输出:<nil> false

	// 如果键存在则读取值,否则储存键值对
	v, ok = m.LoadOrStore("c", 3)
	fmt.Println(v, ok)

	// 删除键值对
	m.Delete("a")

	// 遍历键值对需要提供一个函数,函数可以操作键值。这里只打印
	m.Range(func(key, value interface{}) bool {
		fmt.Println(key, ":", value)
		return true
	})
}

使用 sync.Map 能保证并发安全,且对读操作进行过优化,适合读多写少的场景。