编程 Go maps 包笔记:Equal 遇到 NaN 会返回 false,Clone/Copy 别当深拷贝用

2026-09-08 00:07:23

Go maps 包笔记:Equal 遇到 NaN 会返回 false,Clone/Copy 别当深拷贝用

maps 是 Go 泛型落地后进标准库的那批通用容器操作包之一,专门处理 map[K]V。官方提案见 golang/go discussion #47330,实现与测试分别在 maps.gomaps_test.go

当前 go1.23+ 的 map 工具 API:

  • 复制与合并:CloneCopy
  • 相等判断:EqualEqualFunc
  • 条件删除:DeleteFunc
  • 迭代与转换:KeysValuesAllCollectInsert

其中 KeysValuesAll 面向 range-over-func:Keys/Values 返回 iter.SeqAll 返回 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 == nilClone(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 的变量可以直接传给 CloneCopyEqual 等函数,不需要先转成字面量 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 的内存压力要先评估;有序遍历必须自己排序。

复制全文 生成海报 package maps Go 标准库 泛型

推荐文章

程序员茄子在线接单