Golang之如何(优雅的)比较两个未知结构的json

Stella981
• 阅读 847

这是之前遇到的一道面试题,后来也确实在工作中实际遇到了。于是记录一下,如何(优雅的)比较两个未知结构的json。

假设,现在有两个简单的json文件。

{
    "id":1,
    "name":"testjson01",
    "isadmin":true
}

{
    "isadmin":true,
    "name":"testjson01",
    "id":1    
}

那么,如何比较这两个json的内容是否相同呢?

首先,最基本的方法就是利用golang的反射提供的DeepEqual()

假设我们有一个读取json文件的函数如下:

func LoadJson(path string, dist interface{}) (err error) {
    var content []byte
    if content, err = ioutil.ReadFile(path); err == nil {
        err = json.Unmarshal(content, dist)
    }
    return err
}

那么,我们可以调用该函数来读取json文件。由于json的结构是未知的,所以我们需要声明一个map[string]interface{}来存放对json文件的解析结果。

func main() {
    var (
        json1 map[string]interface{}
        json2 map[string]interface{}
    )
    if err := service.LoadJson("./etc/json/json01.json", &json1); err != nil {
        fmt.Println(err)
    }
    if err := service.LoadJson("./etc/json/json02.json", &json2); err != nil {
        fmt.Println(err)
    }
    fmt.Println(reflect.DeepEqual(json1, json2))
}

这会在终端中输出一个比较的结果:

true

如果我们只需要知道两个json是否相同,那么这样一段简单的代码就可以实现这个要求。

接下来,我们要解决“优雅的”这个定语。

大多数情况下,我们比较两个json,不止需要知道他们是否相同。在他们结构不同的时候,我们还会很自然的关心,他们的区别在哪里。

下面就来解决这个问题。

首先,我们来分析一下json的结构。json作为一个类map的结构体,他的value可能分为3类:

1. json。json的值可能还是json。这就意味着,遇到了值为json的情况,我们需要进行嵌套的比较。另外一点需要注意的,是json结构体本身是无序的,所以比较过程中,要处理好这一点。

2. jsonArray。json的值也有可能是jsonArray。这不仅带来了嵌套比较,还要注意,jsonArray跟json相比,它是有序的。

3. 简单值。这里的简单值包括字符串,实数和布尔值。简单值只需要比较类型和值是否相同即可,也不存在嵌套的情况。

那么思路就清晰了,对于两个json结构体json1和json2,我们首先要遍历json1的键值对,检查json2是否存在对应的键值对,然后根据值的类型分别进行处理。

这里,我们利用golang的反射value.(type)。需要注意的是,value.(type)只能用在switch-case结构中,当我们通过switch判断了value的类型之后,就可以利用断言对其进行类型转换。

在简单值的比较中,因为其不存在结构嵌套的情况,值不同即说明该处存在不同,这样我们就可以用DeepEqual()来简化比较过程。

最后再检查json2中是否存在json1不存在的键值对。

这样,比较是否相同这一目的就达到了。但是目前,这与DeepEqual()并没有不同。所以,我们还需要把整个比较的过程记录下来。对于相同的部分,我们记录json的内容;对于不同的部分,我们分别记录下两者的区别。

type JsonDiff struct {
    HasDiff bool
    Result  string
}

func marshal(j interface{}) string {
    value, _ := json.Marshal(j)
    return string(value)
}

func jsonDiffDict(json1, json2 map[string]interface{}, depth int, diff *JsonDiff) {
    blank := strings.Repeat(" ", (2 * (depth - 1)))
    longBlank := strings.Repeat(" ", (2 * (depth)))
    diff.Result = diff.Result + "\n" + blank + "{"
    for key, value := range json1 {
        quotedKey := fmt.Sprintf("\"%s\"", key)
        if _, ok := json2[key]; ok {
            switch value.(type) {
            case map[string]interface{}:
                if _, ok2 := json2[key].(map[string]interface{}); !ok2 {
                    diff.HasDiff = true
                    diff.Result = diff.Result + "\n-" + blank + quotedKey + ": " + marshal(value) + ","
                    diff.Result = diff.Result + "\n+" + blank + quotedKey + ": " + marshal(json2[key])
                } else {
                    diff.Result = diff.Result + "\n" + longBlank + quotedKey + ": "
                    jsonDiffDict(value.(map[string]interface{}), json2[key].(map[string]interface{}), depth+1, diff)
                }
            case []interface{}:
                diff.Result = diff.Result + "\n" + longBlank + quotedKey + ": "
                if _, ok2 := json2[key].([]interface{}); !ok2 {
                    diff.HasDiff = true
                    diff.Result = diff.Result + "\n-" + blank + quotedKey + ": " + marshal(value) + ","
                    diff.Result = diff.Result + "\n+" + blank + quotedKey + ": " + marshal(json2[key])
                } else {
                    jsonDiffList(value.([]interface{}), json2[key].([]interface{}), depth+1, diff)
                }
            default:
                if !reflect.DeepEqual(value, json2[key]) {
                    diff.HasDiff = true
                    diff.Result = diff.Result + "\n-" + blank + quotedKey + ": " + marshal(value) + ","
                    diff.Result = diff.Result + "\n+" + blank + quotedKey + ": " + marshal(json2[key])
                } else {
                    diff.Result = diff.Result + "\n" + longBlank + quotedKey + ": " + marshal(value)
                }
            }
        } else {
            diff.HasDiff = true
            diff.Result = diff.Result + "\n-" + blank + quotedKey + ": " + marshal(value)
        }
        diff.Result = diff.Result + ","
    }
    for key, value := range json2 {
        if _, ok := json1[key]; !ok {
            diff.HasDiff = true
            diff.Result = diff.Result + "\n+" + blank + "\"" + key + "\"" + ": " + marshal(value) + ","
        }
    }
    diff.Result = diff.Result + "\n" + blank + "}"
}

func jsonDiffList(json1, json2 []interface{}, depth int, diff *JsonDiff) {
    blank := strings.Repeat(" ", (2 * (depth - 1)))
    longBlank := strings.Repeat(" ", (2 * (depth)))
    diff.Result = diff.Result + "\n" + blank + "["
    size := len(json1)
    if size > len(json2) {
        size = len(json2)
    }
    for i := 0; i < size; i++ {
        switch json1[i].(type) {
        case map[string]interface{}:
            if _, ok := json2[i].(map[string]interface{}); ok {
                jsonDiffDict(json1[i].(map[string]interface{}), json2[i].(map[string]interface{}), depth+1, diff)
            } else {
                diff.HasDiff = true
                diff.Result = diff.Result + "\n-" + blank + marshal(json1[i]) + ","
                diff.Result = diff.Result + "\n+" + blank + marshal(json2[i])
            }
        case []interface{}:
            if _, ok2 := json2[i].([]interface{}); !ok2 {
                diff.HasDiff = true
                diff.Result = diff.Result + "\n-" + blank + marshal(json1[i]) + ","
                diff.Result = diff.Result + "\n+" + blank + marshal(json2[i])
            } else {
                jsonDiffList(json1[i].([]interface{}), json2[i].([]interface{}), depth+1, diff)
            }
        default:
            if !reflect.DeepEqual(json1[i], json2[i]) {
                diff.HasDiff = true
                diff.Result = diff.Result + "\n-" + blank + marshal(json1[i]) + ","
                diff.Result = diff.Result + "\n+" + blank + marshal(json2[i])
            } else {
                diff.Result = diff.Result + "\n" + longBlank + marshal(json1[i])
            }
        }
        diff.Result = diff.Result + ","
    }
    for i := size; i < len(json1); i++ {
        diff.HasDiff = true
        diff.Result = diff.Result + "\n-" + blank + marshal(json1[i])
        diff.Result = diff.Result + ","
    }
    for i := size; i < len(json2); i++ {
        diff.HasDiff = true
        diff.Result = diff.Result + "\n+" + blank + marshal(json2[i])
        diff.Result = diff.Result + ","
    }
    diff.Result = diff.Result + "\n" + blank + "]"
}

因为可能会出现,json很长,但是区别只有一两行这种情况,所以我们还需要设定一个输出范围宽度的设定。

当宽度<0时,输出完整的json比较结果。当宽度>=0时,将输出区别范围结果向上下各扩展宽度行的结果。

那么,完整代码如下:

package service

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "reflect"
    "strings"
)

type JsonDiff struct {
    HasDiff bool
    Result  string
}

func JsonCompare(left, right map[string]interface{}, n int) (string, bool) {
    diff := &JsonDiff{HasDiff: false, Result: ""}
    jsonDiffDict(left, right, 1, diff)
    if diff.HasDiff {
        if n < 0 {
            return diff.Result, diff.HasDiff
        } else {
            return processContext(diff.Result, n), diff.HasDiff
        }
    }
    return "", diff.HasDiff
}

func marshal(j interface{}) string {
    value, _ := json.Marshal(j)
    return string(value)
}

func jsonDiffDict(json1, json2 map[string]interface{}, depth int, diff *JsonDiff) {
    blank := strings.Repeat(" ", (2 * (depth - 1)))
    longBlank := strings.Repeat(" ", (2 * (depth)))
    diff.Result = diff.Result + "\n" + blank + "{"
    for key, value := range json1 {
        quotedKey := fmt.Sprintf("\"%s\"", key)
        if _, ok := json2[key]; ok {
            switch value.(type) {
            case map[string]interface{}:
                if _, ok2 := json2[key].(map[string]interface{}); !ok2 {
                    diff.HasDiff = true
                    diff.Result = diff.Result + "\n-" + blank + quotedKey + ": " + marshal(value) + ","
                    diff.Result = diff.Result + "\n+" + blank + quotedKey + ": " + marshal(json2[key])
                } else {
                    diff.Result = diff.Result + "\n" + longBlank + quotedKey + ": "
                    jsonDiffDict(value.(map[string]interface{}), json2[key].(map[string]interface{}), depth+1, diff)
                }
            case []interface{}:
                diff.Result = diff.Result + "\n" + longBlank + quotedKey + ": "
                if _, ok2 := json2[key].([]interface{}); !ok2 {
                    diff.HasDiff = true
                    diff.Result = diff.Result + "\n-" + blank + quotedKey + ": " + marshal(value) + ","
                    diff.Result = diff.Result + "\n+" + blank + quotedKey + ": " + marshal(json2[key])
                } else {
                    jsonDiffList(value.([]interface{}), json2[key].([]interface{}), depth+1, diff)
                }
            default:
                if !reflect.DeepEqual(value, json2[key]) {
                    diff.HasDiff = true
                    diff.Result = diff.Result + "\n-" + blank + quotedKey + ": " + marshal(value) + ","
                    diff.Result = diff.Result + "\n+" + blank + quotedKey + ": " + marshal(json2[key])
                } else {
                    diff.Result = diff.Result + "\n" + longBlank + quotedKey + ": " + marshal(value)
                }
            }
        } else {
            diff.HasDiff = true
            diff.Result = diff.Result + "\n-" + blank + quotedKey + ": " + marshal(value)
        }
        diff.Result = diff.Result + ","
    }
    for key, value := range json2 {
        if _, ok := json1[key]; !ok {
            diff.HasDiff = true
            diff.Result = diff.Result + "\n+" + blank + "\"" + key + "\"" + ": " + marshal(value) + ","
        }
    }
    diff.Result = diff.Result + "\n" + blank + "}"
}

func jsonDiffList(json1, json2 []interface{}, depth int, diff *JsonDiff) {
    blank := strings.Repeat(" ", (2 * (depth - 1)))
    longBlank := strings.Repeat(" ", (2 * (depth)))
    diff.Result = diff.Result + "\n" + blank + "["
    size := len(json1)
    if size > len(json2) {
        size = len(json2)
    }
    for i := 0; i < size; i++ {
        switch json1[i].(type) {
        case map[string]interface{}:
            if _, ok := json2[i].(map[string]interface{}); ok {
                jsonDiffDict(json1[i].(map[string]interface{}), json2[i].(map[string]interface{}), depth+1, diff)
            } else {
                diff.HasDiff = true
                diff.Result = diff.Result + "\n-" + blank + marshal(json1[i]) + ","
                diff.Result = diff.Result + "\n+" + blank + marshal(json2[i])
            }
        case []interface{}:
            if _, ok2 := json2[i].([]interface{}); !ok2 {
                diff.HasDiff = true
                diff.Result = diff.Result + "\n-" + blank + marshal(json1[i]) + ","
                diff.Result = diff.Result + "\n+" + blank + marshal(json2[i])
            } else {
                jsonDiffList(json1[i].([]interface{}), json2[i].([]interface{}), depth+1, diff)
            }
        default:
            if !reflect.DeepEqual(json1[i], json2[i]) {
                diff.HasDiff = true
                diff.Result = diff.Result + "\n-" + blank + marshal(json1[i]) + ","
                diff.Result = diff.Result + "\n+" + blank + marshal(json2[i])
            } else {
                diff.Result = diff.Result + "\n" + longBlank + marshal(json1[i])
            }
        }
        diff.Result = diff.Result + ","
    }
    for i := size; i < len(json1); i++ {
        diff.HasDiff = true
        diff.Result = diff.Result + "\n-" + blank + marshal(json1[i])
        diff.Result = diff.Result + ","
    }
    for i := size; i < len(json2); i++ {
        diff.HasDiff = true
        diff.Result = diff.Result + "\n+" + blank + marshal(json2[i])
        diff.Result = diff.Result + ","
    }
    diff.Result = diff.Result + "\n" + blank + "]"
}

func processContext(diff string, n int) string {
    index1 := strings.Index(diff, "\n-")
    index2 := strings.Index(diff, "\n+")
    begin := 0
    end := 0
    if index1 >= 0 && index2 >= 0 {
        if index1 <= index2 {
            begin = index1
        } else {
            begin = index2
        }
    } else if index1 >= 0 {
        begin = index1
    } else if index2 >= 0 {
        begin = index2
    }
    index1 = strings.LastIndex(diff, "\n-")
    index2 = strings.LastIndex(diff, "\n+")
    if index1 >= 0 && index2 >= 0 {
        if index1 <= index2 {
            end = index2
        } else {
            end = index1
        }
    } else if index1 >= 0 {
        end = index1
    } else if index2 >= 0 {
        end = index2
    }
    pre := diff[0:begin]
    post := diff[end:]
    i := 0
    l := begin
    for i < n && l >= 0 {
        i++
        l = strings.LastIndex(pre[0:l], "\n")
    }
    r := 0
    j := 0
    for j <= n && r >= 0 {
        j++
        t := strings.Index(post[r:], "\n")
        if t >= 0 {
            r = r + t + 1
        }
    }
    if r < 0 {
        r = len(post)
    }
    return pre[l+1:] + diff[begin:end] + post[0:r+1]
}

func LoadJson(path string, dist interface{}) (err error) {
    var content []byte
    if content, err = ioutil.ReadFile(path); err == nil {
        err = json.Unmarshal(content, dist)
    }
    return err
}
点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
2年前
java将前端的json数组字符串转换为列表
记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
2年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Stella981 Stella981
2年前
Gson之实例五
前面四篇博客基本上可以满足我们处理的绝大多数需求,但有时项目中对json有特殊的格式规定.比如下面的json串解析:{"tableName":"students","tableData":{"id":1,"name":"李坤","birthDay":"Jun 22, 2012 9:54:49 PM"},{"id":2,"name":"曹贵生"
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这