← 中文资源

简体中文版本

深度合并配置映射

递归合并多层 JSON 风格配置:嵌套对象逐层组合,标量值由覆盖层替换,并返回可直接序列化的结果。

阅读英文原文

功能说明

应用配置通常来自多层来源:默认配置、环境配置、用户覆盖和命令行参数。简单地把后一层 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))
}