功能说明
应用配置通常来自多层来源:默认配置、环境配置、用户覆盖和命令行参数。简单地把后一层 map 赋给前一层会丢掉未覆盖的嵌套字段,例如覆盖 server.port 时把同级的 server.tls 整个替换掉。深度合并解决这个问题:遇到两边都是对象就递归合并,否则由覆盖层替换基础层。
本例处理 encoding/json 解码后常见的 map[string]any 结构,演示基础配置中的 server 对象包含端口与 TLS 开关,覆盖层只修改端口,合并结果同时保留 TLS。返回值仍然是 map,可直接序列化或继续参与下一层合并。
关键代码
Merge 先处理 dst 为 nil 的情况,创建新映射,使函数既可修改现有值也可作为构造器。遍历源映射时用类型断言判断值是否为嵌套对象;如果是,再检查目标同键是否也是对象,是则递归合并,否则从空映射开始构造,避免把源对象直接放进结果造成共享引用。非对象值直接覆盖。main 构造两层配置并序列化,便于观察嵌套字段是否共存。
使用注意
该函数会修改 dst 指向的映射,调用方若需要保留基础配置,应先做深拷贝。切片不会被逐元素合并,覆盖层的切片会整体替换基础层,这通常符合预期但必须写入文档。数值经 JSON 解码后都是 float64,合并前应做类型校验,避免把字符串误当数字。配置键名大小写应统一,JSON 字段名默认区分大小写。合并顺序决定优先级,应明确从低到高依次调用。
验证方式
运行脚本,输出 JSON 中 server.port 为 9090,server.tls 仍为 false,debug 为 true,name 保持 api,说明嵌套对象被递归合并而标量被覆盖。再给覆盖层增加 server.tls 为 true,重新运行可看到 TLS 被替换。打印 base 可观察它是否被原地修改。
代码示例
package main
import (
"encoding/json"
"fmt"
)
func Merge(dst, src map[string]any) map[string]any {
if dst == nil {
dst = make(map[string]any)
}
for key, value := range src {
if incoming, ok := value.(map[string]any); ok {
if existing, ok := dst[key].(map[string]any); ok {
dst[key] = Merge(existing, incoming)
continue
}
dst[key] = Merge(nil, incoming)
continue
}
dst[key] = value
}
return dst
}
func main() {
base := map[string]any{
"server": map[string]any{"port": 8080.0, "tls": false},
"name": "api",
}
override := map[string]any{
"server": map[string]any{"port": 9090.0},
"debug": true,
}
merged := Merge(base, override)
encoded, _ := json.Marshal(merged)
fmt.Println(string(encoded))
}