Go map 知识点小结

1 minute read

使用 map 的注意事项

  • map 的 key 的类型必须是可比较的,而 value 则可以是任意类型
    • 可比较的类型:boolean,numeric,string,pointer,channel,interface,只包含上述类型的 struct 和 array
    • 不可比较的类型:slice,map,function
  • map 的类型是引用类型(reference type),这一点就像指针和切片。 (这句话有待商榷!)
  • map 的零值是nil,对 nil map 可以进行读操作,但不能进行写操作(会 panic)
  • 对 map 做读操作时,如果 key 不存在,返回的是 value 的零值。 注意:不会报错,这点和其他一些语言如 python 不同。
  • delete(m, key) 函数删除map中的一个 entry:没有返回值;即使 key 不存在也不会报错
  • map 不是并发安全的。如果需要并发读写 map,建议使用读写锁 sync.RWMutex
  • 对 map 进行 for...range 迭代时,迭代的顺序是不定的

关于 map 的类型

maps 和 channels 都不是 reference variables。 更确切地说,Go 中就没有 reference variables —— 两个变量不可能共享相同的内存地址,但是两个变量的内容指向了相同的内存地址是有可能的。

理解 reference variable 可以看这样一个例子

func main() {
    var a int
    var b, c = &a, &a
    fmt.Println(b, c)
    fmt.Println(&b, &c)
    fmt.Println(*b, *c)

    x := 10
    b = &x
    fmt.Println(b, c)
    fmt.Println(&b, &c)
    fmt.Println(*b, *c, a)
}

输出是:

0xc000100010 0xc000100010
0xc000102018 0xc000102020
0 0
0xc000100018 0xc000100010
0xc000102018 0xc000102020
10 0 0

b和c的内容储存了相同的内存地址(即a的地址),但是b和c本身的内存地址是不一样的。 当我们改变了b的内容时,它的值是变量x的内存地址。对b的赋值不会影响c和a的值。

对第二段程序稍作变化:

func main() {
    var a int
    var b, c = &a, &a
    fmt.Println(b, c)
    fmt.Println(&b, &c)
    fmt.Println(*b, *c)

    *b = 100
    fmt.Println(b, c)
    fmt.Println(&b, &c)
    fmt.Println(*b, *c, a)
}

输出如下:

0xc00002c008 0xc00002c008
0xc00000e028 0xc00000e030
0 0
0xc00002c008 0xc00002c008
0xc00000e028 0xc00000e030
100 100 100

这里,代码对 b 的 dereference 赋值,也就是改变了a的值。这样一来,b,c,&b,&c的值都没变,但是b,c 和 a的值变成100了。

话说回来, map 到底是什么类型?

答案是:map 的值是指向 runtime.hmap 结构的指针(A map value is a pointer to a runtime.hmap structure)

可以对照下面的例子加强理解:

func main() {
	m := map[string]int{"originalMap": 1}
	reallocate(m)
	fmt.Println(m) // prints originalMap: 1
	reallocatePtrWrong(&m)
	fmt.Println(m) // prints originalMap: 1
	reallocatePtr(&m)
	fmt.Println(m) // prints overrideMap: 1
	extend(m)
	fmt.Println(m) // prints overrideMap: 1 extended: 2
}

func reallocate(m map[string]int) {
	// Orginal value of m outside function not changed.
	m = map[string]int{"overrideMap": 1}
}

func extend(m map[string]int) {
	// Chaging m without realocation works, and don't need a pointer.
	m["extended"] = 2
}

func reallocatePtrWrong(m *map[string]int) {
	// Realocate using pointer don't become visible outisde if we
	// only update the poniter value.
	m = &map[string]int{"overrideMap": 1}
}

func reallocatePtr(m *map[string]int) {
	// Realocate using pointer work if we do it right.
	*m = map[string]int{"overrideMap": 1}
}

注意,上述的 reallocate 函数是日常写代码时很可能犯的错误,比如当想要清空一个 map 时,使用这类在函数内部赋值的操作,对外部是无效的。

参考

Go maps in action There is no pass-by-reference in Go

If a map isn’t a reference variable, what is it?

延伸阅读

Go 中 map 的实现细节: How the Go runtime implements maps efficiently (without generics)

Tags: ,

Categories:

Updated:

Comments