Go maps 包笔记:Equal 遇到 NaN 会返回 false,Clone/Copy 别当深拷贝用
maps 是 Go 泛型落地后进标准库的那批通用容器操作包之一,专门处理 map[K]V。官方提案见 golang/go discussion #47330,实现与测试分别在 maps.go 和 maps_test.go。
当前 go1.23+ 的 map 工具 API:
- 复制与合并:
Clone、Copy - 相等判断:
Equal、EqualFunc - 条件删除:
DeleteFunc - 迭代与转换:
Keys、Values、All、Collect、Insert
其中 Keys、Values、All 面向 range-over-func:Keys/Values 返回 iter.Seq,All 返回 iter.Seq2。go1.23 之前的版本里,Keys/Values 返回的是 []K/[]V slice,升级后要注意调用方式变化。
maps.Equal 对 NaN 没有魔法
Equal 的内部逻辑就是遍历 m1,用 key 去 m2 里查,然后比较 value。这个“相等”全部落在 == 上,而 NaN 对自己都不相等:
- value 是 NaN:同一个 key 能找到,但
v1 != v2成立,判断为不相等; - key 是 NaN:拿这个 key 去 m2 里做 map lookup,同样命中不了。
结果就是 maps.Equal(m, m) 也可能返回 false,标准库测试里明确断言过这个行为。
如果 value 可能是 float/NaN,要用 EqualFunc 自己在 eq 函数里处理 NaN。注意 EqualFunc 只接管 value 比较,key 的查找仍然靠 ==,所以 NaN 出现在 key 上时它也救不了。
Clone 是浅拷贝,并且保留 nil
官方注释写得很明确:新 map 的 key/value 使用 ordinary assignment 赋值,只复制顶层键值。value 里是指针、slice、map 或带引用的结构体时,新旧 map 共享底层对象;要深拷贝必须自己递归。
另一个容易踩的点:m == nil 时 Clone(m) 返回 nil,不会返回一个空 map。
Copy 不会清空 dst 里多出来的键
maps.Copy(dst, src) 把 src 覆盖进 dst,dst 里已有、src 里没有的 key 会保留。如果想让 dst 最终严格等于 src,必须先 clear(dst),再 Copy(dst, src)。
Go 1.21 起 clear 已经是 builtin;旧代码用 for-range delete 也行。
Keys/Values/All 顺序不保证
这几个函数直接基于 map range,map 的迭代顺序本来就不确定,官方也不承诺两次函数调用返回的顺序一致。
要稳定输出就显式排序:
sortedKeys := slices.Sorted(maps.Keys(m))
或者先收到 slice,再 slices.Sort。
DeleteFunc 可以在遍历中删
DeleteFunc(m, func(k K, v V) bool { ... }) 的语义是:predicate 返回 true 就删掉这对键值。Go 允许在遍历 map 时删除当前元素,所以它是可以安全边 range 边 delete 的。
All/Collect/Insert 是 map 和 iter.Seq2 之间的连接器
maps.All(m)产生iter.Seq2[K, V];maps.Collect(seq)把iter.Seq2收成 map,遇到重复 key 时后者覆盖前者;maps.Insert(dst, maps.All(src))等价于一次Copy。
这里容易搞混:slices.Collect 接收的是 iter.Seq,不是 iter.Seq2。想要一对键值收成 slice,应该 slices.Collect(maps.Keys(m)) 或者 slices.Collect(maps.Values(m));All 出来的 Seq2 对应的是 maps.Collect。
自定义 map 类型可以直接传
maps 里所有泛型约束都是 M ~map[K]V 这种底层类型约束,所以:
type MyMap map[string]int
MyMap 的变量可以直接传给 Clone、Copy、Equal 等函数,不需要先转成字面量 map[string]int。
用一段代码验证上面几个点
package main
import (
"fmt"
"maps"
"math"
"slices"
)
func eqFloat(a, b float64) bool {
if math.IsNaN(a) || math.IsNaN(b) {
return math.IsNaN(a) && math.IsNaN(b)
}
return a == b
}
func main() {
m := map[string]float64{"price": math.NaN()}
fmt.Println("Equal: ", maps.Equal(m, m)) // false
fmt.Println("EqualFunc: ", maps.EqualFunc(m, m, eqFloat)) // true
var nilMap map[string]int
fmt.Println("Clone(nil): ", maps.Clone(nilMap) == nil) // true
km := map[string]int{"c": 3, "a": 1}
fmt.Println("sorted keys: ", slices.Sorted(maps.Keys(km))) // [a c]
}
这份代码要求 Go 1.23+。
通用 value 语义操作能用 maps 直接做的,就不值得每个人再各自维护一套 for-range 拷贝或手写相等比较。但有几类取舍它替你决定不了:并发写 map 仍然要靠锁或 sync.Map;超大 map 频繁 Clone 的内存压力要先评估;有序遍历必须自己排序。