我有一个递归函数,可以创建表示文件路径的对象(键是路径,值是有关文件的信息)。它是递归的,因为它仅用于处理文件,因此如果遇到目录,则会在目录上递归调用该函数。
话虽这么说,我想在两个映射上执行相当于集合并集的操作(即用递归调用的值更新“主”映射)。除了迭代一个映射并将其中的每个键、值分配给另一个映射中的相同内容之外,是否有一种惯用的方法可以做到这一点?
也就是说:给定
a,b
的类型为 map [string] *SomeObject
,并且最终填充 a
和 b
,有没有办法用 a
中的所有值更新 b
?
没有内置方法,标准包中也没有任何方法可以进行此类合并。
理想的方法是简单地迭代:
for k, v := range b {
a[k] = v
}
从 Go 1.21 开始,您可以简单地使用新的
maps.Copy
功能:
package main
import (
"fmt"
"maps"
)
func main() {
src := map[string]int{
"one": 1,
"two": 2,
}
dst := map[string]int{
"two": 42,
"three": 3,
}
maps.Copy(dst, src)
fmt.Println("src:", src)
fmt.Println("dst:", dst)
}
(游乐场)
输出:
src: map[one:1 two:2]
dst: map[one:1 three:3 two:2]
Copy
包中的 golang.org/x/exp/maps
函数:
package main
import (
"fmt"
"golang.org/x/exp/maps"
)
func main() {
src := map[string]int{
"one": 1,
"two": 2,
}
dst := map[string]int{
"two": 42,
"three": 3,
}
maps.Copy(dst, src)
fmt.Println("src:", src)
fmt.Println("dst:", dst)
}
(游乐场)
输出:
src: map[one:1 two:2]
dst: map[one:1 three:3 two:2]
这种方法的一个警告是,在 Go 版本 1.18.x 到 1.19.x 中,映射的键类型必须是 concrete,即不是接口类型。例如,编译器不允许您将
map[io.Reader]int
类型的值传递给 Copy
函数:
package main
import (
"fmt"
"io"
"golang.org/x/exp/maps"
)
func main() {
var src, dst map[io.Reader]int
maps.Copy(dst, src)
fmt.Println("src:", src)
fmt.Println("dst:", dst)
}
编译器输出:
go: finding module for package golang.org/x/exp/maps
go: downloading golang.org/x/exp v0.0.0-20220328175248-053ad81199eb
./prog.go:12:11: io.Reader does not implement comparable
Go build failed.
Go 1.20(playground)中取消了此限制。
从 go 1.18 开始,由于泛型功能的发布,现在有了联合映射的泛型函数!
您可以使用像 https://github.com/samber/lo 这样的包来执行此操作。 请注意,键可以是任何“可比较”类型,而值可以是任何类型。
示例:
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
map1 := map[string]interface{}{"k1": "v1", "k2": 2}
map2 := map[string]interface{}{"k2": "v2new", "k3": true}
map1 = lo.Assign(map1, map2)
fmt.Printf("%v", map1)
}
结果是:
map[k1:v1 k2:v2new k3:true]
如果您有几个嵌套映射,
left
和right
,那么此函数将递归地将right
中的项目添加到left
中。如果密钥已经在 left
中,那么我们会更深入地递归到结构中,并仅尝试将 add 键添加到 left
(例如,永远不要替换它们)。
type m = map[string]interface{}
// Given two maps, recursively merge right into left, NEVER replacing any key that already exists in left
func mergeKeys(left, right m) m {
for key, rightVal := range right {
if leftVal, present := left[key]; present {
//then we don't want to replace it - recurse
left[key] = mergeKeys(leftVal.(m), rightVal.(m))
} else {
// key not in left so we can just shove it in
left[key] = rightVal
}
}
return left
}
注意:我不处理该值本身不是
map[string]interface{}
的情况。因此,如果您有 left["x"] = 1
和 right["x"] = 2
,那么上面的代码在尝试 leftVal.(m)
时将会出现恐慌。
这是另一种选择,
github.com/samber/lo
,ORgolang.org/x/exp
的实验性质不满意(请阅读警告),或者append()
的 API,而不是来自 exp.Copy()
的 golang.org/x/exp
(append 接受任意数量的列表,而 Copy()
仅接受 2 个列表)。但是它需要 Go 1.18+,因为它使用 go 泛型。
将以下内容保存在您的模块/包之一中:
func MergeMaps[M ~map[K]V, K comparable, V any](src ...M) M {
merged := make(M)
for _, m := range src {
for k, v := range m {
merged[k] = v
}
}
return merged
}
然后你就可以像
append()
一样使用它:
func main() {
mergedMaps := MergeMaps(
map[string]int{"a": 1, "b": 2},
map[string]int{"b": 3, "c": 4},
map[string]int{"c": 3, "d": 4},
)
fmt.Println(mergedMaps)
}
Go 受到地图类型的限制。我怀疑没有内置函数,因为映射可能存在无限数量的类型声明。因此,您必须根据您使用的地图类型构建自己的合并函数:
func MergeJSONMaps(maps ...map[string]interface{}) (result map[string]interface{}) {
result = make(map[string]interface{})
for _, m := range maps {
for k, v := range m {
result[k] = v
}
}
return result
}