Go基于反射实现任意结构json值的替换
最近面临一个应用场景,需要对一个倒手转发的json进行数据替换,查阅了众多的库,大部分都不支持这个功能,从历史项目里捞出了jsonpath这个库,但其只能根据jsonpath的schema进行Get操作,无法实现inplace回写,后来又找到sjson,这是个根据jsonpath回写的库,没有用反射,自己实现的parser,但是在数组、对象混合嵌套的结构中,没办法正确写入,因为在使用gjson提取值之后,已经丢失了原始路径信息。在经过一番折腾之后,还是决定自己写一个替换器,虽然反射效率会比较低,但是好歹能实现。
需求
先抛弃jsonpath的思路,第一版先从简单的开始,期望替换库具备以下功能
- 可以根据给定的key名,替换所有该key名下的value
- 可以根据给定的key值类型,替换所有该类型的value
- 以上三种方式的组合
- 可以控制替换的深度
举例来说,
case1:对于下面这个json,
{
"name1": "Bob",
"name2": "Alice"
}
可以通过指定key=name2,将Alice增加一个前缀
{
"name1": "Bob",
"name2": "hello Alice"
}
case2:对于下面的json,指定替换字符串类型,并控制替换深度为2
{
"name1": "Bob",
"tag1": {
"name2": "Dave",
"tag2": {
"name3": "Alice"
}
}
}
替换后为
{
"name1": "hello Bob",
"tag1": {
"name2": "hello Dave",
"tag2": {
"name3": "Alice"
}
}
}
思路
由于json结构未知,无法直接Unmarshal为确定的结构,所以将json文档Unmarshal为map[string]interface{}进行解析。
由于json的深度未知,遂通过递归的方式,逐级遍历完成替换。
实现
数据结构
声明一个结构replacer,用于表示替换过程,其结构如下:
type replacer struct {
maxDepth int // 最大递归替换深度,如果为0,表示不限制递归深度
kind reflect.Kind // 匹配的字段类型,如果未reflect.Invalid,表示不关心类型
keyword []string // 匹配的Key名,如果未指定即为nil,则仅做类型匹配
replacer func(interface{}) interface{} // 替换器
currDepth int // 当前递归深度
}
前三个字段,对应了需求中的约束条件,如果kind和keyword都为默认值,则所有值都将被替换;replacer是自定义替换函数,符合约束条件的值将会通过replacer调用传递给使用者,由使用者完成自定替换过程。currDepth为递归深度控制变量。
预处理
func (r replacer) Replacing(content []byte) ([]byte, error) {
// 校验是否合法json
if !json.Valid(content) {
return nil, errors.New("invalid json")
}
// 反序列化json,兼容root为对象或数组的场景
var holder interface{}
if content[0] == '{' {
holder = map[string]interface{}(nil)
} else if content[0] == '[' {
holder = []interface{}(nil)
}
err := json.Unmarshal(content, &holder)
if err != nil {
return nil, err
}
// 遍历/替换过程
return json.Marshal(r.replacingElements("", holder))
}
输入的json为未知结构,所以首先对json合法性进行判断,将json反序列化为interface based结构。
替换的关键逻辑由replacingElements方法实现,并将替换后的json内容序列化为字符串返回。
替换逻辑
先来看一下replacingElements的伪代码
func replacingElements(key string, value interface{})interface{}{
// 1.判断深度,达到递归深度则结束递归
// 2.获取value的类型信息
// 3.如果是值类型(字符串、数字、布尔),判断是否为指定的kind,且key是否为期望的key,如果是,则替换并回写
// 4.如果是数组类型,遍历数组成员,对成员进行replaceElements递归调用
// 5.如果是对象类型,遍历对象下的成员,对成员进行replaceElements递归调用
// 6.如果没有指定的kind,则默认将所有kv都进行替换
// 7.返回替换后的文档
}
深度判断
func (r replacer) deepEnough() bool {
// 如果maxDepth为0,表示不限制递归深度,总返回false
// 如果maxDepth大于0,且当前深度没有达到maxDepth,则返回false
// 否则返回true
return r.maxDepth != 0 && r.currDepth > r.maxDepth
}
是否为待替换key的判定
func (r replacer) needReplace(key string) bool {
// 如果使用者没有指定keyword,那么总是返回true,表示不需要区分key
if r.keyword == nil || len(r.keyword) == 0 {
return true
}
// 查看当前key是否是使用者指定的key集合中的一个
for _, v := range r.keyword {
if v == key {
return true
}
}
return false
}
Number类型值处理
json中Number可以表达所有的数字,包括整形、无符号数、浮点数等。而在go中,对Number类型的反射,总是解释为float64类型,如果需要匹配使用者期望的kind,则需要对类型进行转化,实现如下
func (r replacer) convertArbitraryNumber(n interface{}, expect reflect.Kind) interface{} {
// 获取以float64表达的json Number类型值
rn, ok := n.(float64)
if !ok {
return n
}
// 将以float64表达的Number转换为目标类型,目标类型由使用者指定
switch expect {
case reflect.Float32:
return float32(rn) // precision lost, cut off
case reflect.Int:
return int(rn) // precision lost
case reflect.Int8:
return int8(rn) // cut off
case reflect.Int16:
return int16(rn) // cut off
case reflect.Int32:
return int32(rn) // cut off
case reflect.Int64:
return int64(rn) // precision lost
case reflect.Uint:
return uint(rn) // cut off, sign lost
case reflect.Uint8:
return uint8(rn) // cut off, sign lost
case reflect.Uint16:
return uint16(rn) // cut off, sign lost
case reflect.Uint32:
return uint32(rn) // cut off, sign lost
case reflect.Uint64:
return uint64(rn) // precision lost, sign lost
}
return rn
}
需要注意float64转换为指定类型时,可能会有精度丢失、符号丢失的问题,所以最好是对替换目标值的类型心里有数。
替换过程
这里是json替换的核心逻辑
func (r replacer) replacingElements(key string, value interface{}) interface{} {
// 递归深度判断
if r.deepEnough() {
return value
}
// 反射获取value的类型信息
vt := reflect.TypeOf(value)
k := vt.Kind()
// 根据kind分别处理
switch k {
case r.kind: // 是期望的值类型
if r.needReplace(key) {
// 当前key是目标key集合中的内容
return r.replacer(value)
}
case reflect.Slice:
// 获得slice的value
val := reflect.ValueOf(value)
r.currDepth++ // increace depth
for i := 0; i < val.Len(); i++ {
// 遍历slice
elem := val.Index(i)
if elem.CanInterface() {
// 将成员递归处理
relem := r.replacingElements(key, elem.Interface())
if elem.CanSet() {
elem.Set(reflect.ValueOf(relem))
}
}
}
case reflect.Map:
val, ok := value.(map[string]interface{})
if ok {
r.currDepth++ // increace depth
for mkey, mval := range val {
// 将成员递归处理
val[mkey] = r.replacingElements(mkey, mval)
}
}
default:
if k == reflect.Float64 && (r.kind >= reflect.Int && r.kind <= reflect.Float32) {
// for arbitrary numberic types, adjust underlying type to expected
if r.needReplace(key) {
// 这里调用convertArbitraryNumber对float64进行了转换,确保使用者在对value进行断言时
// 可以获得自己在replacer.kind成员上指定的类型值
return r.replacer(r.convertArbitraryNumber(value, r.kind))
}
} else if r.kind == reflect.Invalid {
// 对于任意类型的值,不判断value的类型
if r.needReplace(key) {
return r.replacer(value)
}
}
}
return value
}
接口化
这里借鉴了我之前在《redis分布式锁与redsync库源码分析》中提到的可变参Option的技巧
func NewReplacer(replaceFunc func(interface{}) interface{}, options ...Option) IReplacer {
r := new(replacer)
for _, v := range options {
v.Apply(r)
}
r.replacer = replaceFunc
return r
}
type Option interface {
Apply(*replacer)
}
type OptionFunc func(*replacer)
func (f OptionFunc) Apply(r *replacer) {
f(r)
}
func WithMaxDepth(n int) Option {
return OptionFunc(func(r *replacer) {
r.maxDepth = n
})
}
func WithKind(k reflect.Kind) Option {
return OptionFunc(func(r *replacer) {
r.kind = k
})
}
func WithKeyword(k string) Option {
return OptionFunc(func(r *replacer) {
if r.keyword == nil {
r.keyword = make([]string, 0, 1)
}
r.keyword = append(r.keyword, k)
})
}
func WithKeywords(k []string) Option {
return OptionFunc(func(r *replacer) {
if r.keyword == nil {
r.keyword = make([]string, 0, 1)
}
r.keyword = append(r.keyword, k...)
})
}
type IReplacer interface {
Replacing([]byte) ([]byte, error)
}
使用起来就很简单,示例如下:
replaceFunc := func(i interface{}) interface{} {
val, ok := i.(string)
if ok {
return "hello " + val
}
return i
},
r := NewReplacer(replaceFunc, WithKind(reflect.String), WithKeyword("name"))
ret := r.Replacing([]byte(`{"name1":"Bob","name2":"Alice"}`))
fmt.Println(string(ret)) // {"name1":"hello Bob","name2":"hello Alice"}
这样,一个基于反射的json值替换器就实现完了。
结语
这个是一次对反射功能的实践,目的是加深对go反射的理解。从性能角度来讲,大概率是不如gjson/sjson这种基于文本parser的实现的。不过重点在于对反射原则的理解,能不能取用您自己看着办吧。

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异