返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

5.4 字典

发布于 2024-10-12 19:15:47 字数 4388 浏览 0 评论 0 收藏 0

字典(哈希表)存储键值对,一种使用频率极高的数据结构。

   pointer              header
   
  +-------+          +-----------+
  |  map -|--------> |  hmap     |
  +-------+          +-----------+
                     |  ...      |
                     +-----------+       +-----//-----+
                     |  buckets -|-----> | ...    ... |   array
                     +-----------+       +-----//-----+
  • 引用类型,无序键值对集合。
  • 以初始化表达式或 make 函数创建。
  • 主键须是支持 ==!= 的类型。
  • 可判断字典是否为 nil ,不支持比较操作。
  • 函数 len 返回键值对数量。
  • 访问不存在主键,返回零值。
  • 迭代以随机次序返回。
  • 值不可寻址(not addressable),需整体赋值。
  • 按需扩张,但不会收缩(shrink)内存。
func p(m map[string]int) {
    fmt.Printf("%t, %x\n", m == nil,
               *(*uintptr)(unsafe.Pointer(&m)))
}

func main() {
    
    // 字典变量是指针类型。
    // 未初始化,那就是空指针。
    
    var m1 map[string]int
    m2 := map[string]int{}
    m3 := make(map[string]int, 0)
    
    p(m1)
    p(m2)
    p(m3)
}

/*

m1: true,  0
m2: false, c000064da0
m3: false, c000064d70

*/
func main() {
	m1 := map[string]int {
		"a": 1,
		"b": 2,
	}

	// 省略复合类型标签。
	m2 := map[string]struct {
		id   int
		name string
	}{
		"a": { 1, "u1" },
		"b": { 2, "u2" },
	}

    // 分配足够初始容量,可减少后续扩张。
	m3 := make(map[string]int, 10)
	m3["a"] = 1

	fmt.Println(m1, len(m1))
	fmt.Println(m2, len(m2))
	fmt.Println(m3, len(m3))
}

/*

m1: map[a:1 b:2]            len = 2
m2: map[a:{1 u1} b:{2 u2}]  len = 2
m3: map[a:1] 1              len = 1

*/
func main() {
	m1 := map[int]int{}
	m2 := map[int]int{}

	// _ = m1 == m2
	//     ~~~~~~~~ invalid: map can only be compared to nil
}
  • 建议以 ok-idiom 访问主键,确认是否存在。
  • 删除( delete )不存在键值,不会引发错误。
  • 清空( clear )字典,不会收缩内存。
  • 可对 nil 字典读、删除,但写会引发 panic
func main() {
	m := map[string]int{}

	// 是否有 { "a": 0 } 存在?
    
	x := m["a"]
	fmt.Println(x)       // 0

	x, ok := m["a"]
	fmt.Println(x, ok)   // 0 false
	
	m["a"] = 0
	x, ok = m["a"]
	fmt.Println(x, ok)   // 0 true
}
func main() {
    var m map[string]int  // nil
	
	_ = m["a"]
	delete(m, "a")

	// m["a"] = 0
	// ~~~~~~ panic: assignment to entry in nil map
}
func main() {
    m := map[string]int {
    	"a": 1,
    	"b": 2,
    }

	println(len(m))	// 2

	clear(m)
	println(len(m))	// 0
}

迭代字典,以 “随机” 次序返回键值。

  • struct{} 为可忽略值类型。
  • 随机效果依赖键值对数量和迭代次数。
  • 可用作简易版随机挑选算法。
func main() {
	m := make(map[int]struct{})

	for i := 0; i < 10; i++ {
		m[i] = struct{}{}
	}

	for i := 0; i < 4; i++ {
		for k, _ := range m {
			print(k, ",")
		}

		println()
	}
}

/*

7,2,3,4,5,9,0,1,6,8,
1,6,8,9,0,3,4,5,7,2,
0,1,6,8,9,2,3,4,5,7,
9,0,1,6,8,7,2,3,4,5,

*/

内联存储键值迁移内存安全 需要,字典被设计成不可寻址。
不能直接修改值成员(结构或数组),应整体赋值,或以指针代替。

内联存储有更好的性能,减少 GC 压力。详情请参考《下卷:8.4 字典》。

func main() {
	type user struct {
		name string
		age  byte
	}

    // 指针。
	m := map[int]*user{
		1: &user{"u1", 19},
	}

    // 返回指针,修改外部对象。
	m[1].age = 20
}
func main() {
	m := map[string]int{ 
        "a": 1 
    }

	m["a"]++    // m["a"] = m["a"] + 1
}

安全

迭代期间,新增或删除操作是安全的,但无法控制次序。

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

	for i := 0; i < 10; i++ {
		m[i] = i + 10
	}

	for k := range m {
		if k == 5 {
			m[100] = 1000
		}

		delete(m, k)
		fmt.Println(k, m)
	}
}

运行时会对字典并发操作做出检测。

  • 启用竞争检测(data race)查找此类问题。
  • 使用 sync.Map 代替。
package main

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

	// write
	go func() {
		for {
			m["a"] += 1
		}
	}()

	// read
	go func() {
		for {
			_ = m["b"]
		}
	}()

	select {}
}

// fatal error: concurrent map read and map write
$ go build -race && ./test

==================
WARNING: DATA RACE

Write at 0x00c000078150 by goroutine 7:
      main.go:9 +0x7b

Previous read at 0x00c000078150 by goroutine 8:
      main.go:16 +0x44

Goroutine 7 (running) created at:
  main.main()
      main.go:7 +0x84

Goroutine 8 (running) created at:
  main.main()
      main.go:14 +0xd4
      
==================

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文