首页 > 后端开发 > Golang > 正文

Go语言中json.RawMessage的正确使用:避免嵌套JSON解码陷阱

DDD
发布: 2025-11-01 14:18:01
原创
716人浏览过

Go语言中json.RawMessage的正确使用:避免嵌套JSON解码陷阱

本文深入探讨了在go语言中处理嵌套json数据时,json.rawmessage类型在结构体中直接使用可能导致的解码失败问题。核心在于json.rawmessage的特殊性,其unmarshaljson等方法需要指针接收者。文章通过示例代码演示了将json.rawmessage声明为*json.rawmessage的解决方案,确保json数据能够被正确地封送和解封,从而有效保留原始json片段并顺利进行二次解析。

引言:json.RawMessage的作用与常见误区

在Go语言中,encoding/json包提供了强大的JSON序列化和反序列化能力。json.RawMessage是一个特殊的类型,它被设计用来处理那些我们不希望立即解析,或者其结构在运行时才确定的JSON子片段。当一个结构体字段被定义为json.RawMessage时,encoding/json在进行反序列化时会将其对应的值作为一个原始的字节切片保留下来,而不尝试对其内容进行进一步的解析。这在需要延迟解析、动态解析或仅仅是传递JSON片段的场景中非常有用。

然而,直接在结构体中使用json.RawMessage作为字段类型,而非其指针形式,是Go JSON处理中的一个常见陷阱。这会导致在反序列化时,原本期望的原始JSON数据被错误地编码成一个字符串,从而在后续尝试解析该字段时遇到解码失败。

问题复现:json.RawMessage解码失败的场景

为了更好地理解这个问题,我们首先通过一个示例代码来复现这种错误的场景。假设我们有一个外部结构体DataWithRawMessageIncorrect,其中包含一个Json字段,类型为json.RawMessage,用于存储一个内部结构体InnerData的JSON表示。

package main

import (
    "encoding/json"
    "fmt"
)

// InnerData 代表内部JSON的结构
type InnerData struct {
    Name string
    Id   int
}

// DataWithRawMessageIncorrect 外部结构体,Json字段类型为json.RawMessage (错误示范)
type DataWithRawMessageIncorrect struct {
    Name string
    Id   int
    Json json.RawMessage // 问题所在:应为 *json.RawMessage
}

func main() {
    // 1. 封送一个内部结构体实例
    inner := InnerData{"World", 2}
    innerBytes, err := json.Marshal(inner)
    if err != nil {
        fmt.Printf("封送InnerData失败: %s\n", err)
        return
    }
    fmt.Printf("原始内部JSON: %s\n", string(innerBytes)) // 预期: {"Name":"World","Id":2}

    // 2. 将内部JSON字节切片嵌入到外部结构体中并封送
    outerIncorrect := DataWithRawMessageIncorrect{"Hello", 1, innerBytes}
    outerIncorrectBytes, err := json.Marshal(outerIncorrect)
    if err != nil {
        fmt.Printf("封送DataWithRawMessageIncorrect失败: %s\n", err)
        return
    }
    // 观察这里的输出,Json字段的值被双引号包裹,且内容是Base64编码的
    fmt.Printf("错误封送结果: %s\n", string(outerIncorrectBytes)) 
    // 预期: {"Name":"Hello","Id":1,"Json":"eyJOYW1lIjoiV29ybGQiLCJJZCI6Mn0="}

    // 3. 将错误封送结果反序列化回DataWithRawMessageIncorrect
    var unmarshaledIncorrect DataWithRawMessageIncorrect
    err = json.Unmarshal(outerIncorrectBytes, &unmarshaledIncorrect)
    if err != nil {
        fmt.Printf("反序列化DataWithRawMessageIncorrect失败: %s\n", err)
        return
    }
    // 观察d.Json的内容,它仍然是一个被引号包裹的字符串(Base64编码)
    fmt.Printf("解封后d.Json内容: %s\n", string(unmarshaledIncorrect.Json))
    // 预期: "eyJOYW1lIjoiV29ybGQiLCJJZCI6Mn0="

    // 4. 尝试将d.Json的内容再次反序列化为InnerData (将失败)
    var reParsedInner InnerData
    err = json.Unmarshal(unmarshaledIncorrect.Json, &reParsedInner) // 这里会报错
    if err != nil {
        // 报错信息:json: cannot unmarshal string into Go value of type main.InnerData
        fmt.Printf("尝试解封d.Json到InnerData失败: %s\n", err.Error())
    }
    fmt.Printf("错误解封结果: %+v\n", reParsedInner) // 预期: { 0} (空值)
}
登录后复制

运行上述代码,你会看到类似以下输出:

立即学习go语言免费学习笔记(深入)”;

原始内部JSON: {"Name":"World","Id":2}
错误封送结果: {"Name":"Hello","Id":1,"Json":"eyJOYW1lIjoiV29ybGQiLCJJZCI6Mn0="}
解封后d.Json内容: "eyJOYW1lIjoiV29ybGQiLCJJZCI6Mn0="
尝试解封d.Json到InnerData失败: json: cannot unmarshal string into Go value of type main.InnerData
错误解封结果: { 0}
登录后复制

从输出中可以清楚地看到问题:

  1. 在将outerIncorrect封送为JSON时,Json字段的值{"Name":"World","Id":2}被Base64编码成了eyJOYW1lIjoiV29ybGQiLCJJZCI6Mn0=,并作为字符串嵌入到外部JSON中。
  2. 反序列化后,unmarshaledIncorrect.Json中存储的仍然是这个被双引号包裹的Base64编码字符串。
  3. 当我们尝试将unmarshaledIncorrect.Json反序列化为InnerData时,encoding/json发现它是一个JSON字符串(带引号),而不是一个原始的JSON对象或数组,因此报告cannot unmarshal string into Go value of type main.InnerData错误。

根源分析:json.RawMessage的指针接收者

这个问题的根源在于json.RawMessage类型在encoding/json包中的特殊处理机制,特别是其UnmarshalJSON方法的定义。

json.RawMessage的底层类型是[]byte。在Go语言中,当一个类型实现了json.Marshaler和json.Unmarshaler接口时,encoding/json包会调用这些自定义方法来进行序列化和反序列化。json.RawMessage正是通过实现这些接口来达到其特殊目的的。

关键在于,json.RawMessage的UnmarshalJSON方法是定义在一个指针接收者上的,即func (m *RawMessage) UnmarshalJSON(data []byte) error。

Find JSON Path Online
Find JSON Path Online

Easily find JSON paths within JSON objects using our intuitive Json Path Finder

Find JSON Path Online30
查看详情 Find JSON Path Online

当encoding/json在反序列化一个结构体时,如果遇到一个字段类型是json.RawMessage(非指针),它不会调用*json.RawMessage上的UnmarshalJSON方法。相反,它会将其视为一个普通的[]byte类型。如果对应的JSON值是一个字符串(例如"some_string_value"),encoding/json会尝试将这个字符串的Base64解码结果(或者直接的字符串内容)存入[]byte字段。但在这里,我们期望的是将一个JSON对象{"key":"value"}作为原始字节存入。

由于json.RawMessage字段被当作普通的[]byte处理,当它遇到一个JSON对象或数组时,它会先将其作为JSON的进行处理。如果这个值是一个嵌套的JSON结构,encoding/json在将其存入非指针的json.RawMessage字段时,会把它当作一个普通的字符串来处理,即对内部的JSON数据进行Base64编码,然后用双引号包裹起来,形成一个JSON字符串。这就是为什么我们在错误封送结果中看到"Json":"eyJOYW1lIjoiV29ybGQiLCJJZCI6Mn0="。

在反序列化回DataWithRawMessageIncorrect时,unmarshaledIncorrect.Json接收到的就是这个带引号的Base64编码字符串。当再尝试对unmarshaledIncorrect.Json进行解析时,encoding/json看到的是一个字符串字面量(例如"{\"Name\":\"World\",\"Id\":2}"),而不是一个原始的JSON对象{"Name":"World","Id":2}。因此,它无法将其反序列化到非字符串类型的InnerData结构体中。

解决方案:使用*json.RawMessage

解决这个问题的关键非常简单:将结构体中的json.RawMessage字段声明为指针类型,即*json.RawMessage。

当结构体字段类型为*json.RawMessage时,encoding/json在反序列化时会识别出这是一个指针类型,并会尝试调用*json.RawMessage上的UnmarshalJSON方法。这个方法会确保将对应的JSON值作为一个原始的字节切片正确地赋值给*json.RawMessage所指向的内存,而不会进行额外的字符串编码或Base64转换。

正确实践:完整示例代码

下面是修正后的代码示例,展示了如何正确地使用*json.RawMessage来处理嵌套JSON:

package main

import (
    "encoding/json"
    "fmt"
)

// InnerData 代表内部JSON的结构
type InnerData struct {
    Name string
    Id   int
}

// DataWithRawMessageCorrect 外部结构体,Json字段类型为 *json.RawMessage (正确示范)
type DataWithRawMessageCorrect struct {
    Name string
    Id   int
    Json *json.RawMessage // 修正:使用指针类型
}

func main() {
    // 1. 封送一个内部结构体实例
    inner := InnerData{"World", 2}
    innerBytes, err := json.Marshal(inner)
    if err != nil {
        fmt.Printf("封送InnerData失败: %s\n", err)
        return
    }
    fmt.Printf("原始内部JSON: %s\n", string(innerBytes)) // 预期: {"Name":"World","Id":2}

    // 2. 将内部JSON字节切片嵌入到外部结构体中并封送
    // 注意:在赋值给 *json.RawMessage 字段时,需要先创建一个 json.RawMessage 实例并取其地址
    rawMsg := json.RawMessage(innerBytes)
    outerCorrect := DataWithRawMessageCorrect{"Hello", 1, &rawMsg} // 传入指针

    outerCorrectBytes, err := json.Marshal(outerCorrect)
    if err != nil {
        fmt.Printf("封送DataWithRawMessageCorrect失败: %s\n", err)
        return
    }
    // 观察这里的输出,Json字段的值现在是原始的JSON对象,没有被引号包裹
    fmt.Printf("正确封送结果: %s\n", string(outerCorrectBytes)) 
    // 预期: {"Name":"Hello","Id":1,"Json":{"Name":"World","Id":2}}

    // 3. 将正确封送结果反序列化回DataWithRawMessageCorrect
    var unmarshaledCorrect DataWithRawMessageCorrect
    err = json.Unmarshal(outerCorrectBytes, &unmarshaledCorrect)
    if err != nil {
        fmt.Printf("反序列化DataWithRawMessageCorrect失败: %s\n", err)
        return
    }
    // 观察d.Json的内容,它现在是原始的JSON字节切片
    // 注意:由于 Json 是 *json.RawMessage,我们需要解引用才能得到实际的 []byte 内容
    if unmarshaledCorrect.Json != nil {
        fmt.Printf("解封后d.Json内容: %s\n", string(*unmarshaledCorrect.Json))
        // 预期: {"Name":"World","Id":2}
    } else {
        fmt.Println("解封后d.Json为nil")
    }


    // 4. 尝试将d.Json的内容再次反序列化为InnerData (现在将成功)
    var reParsedInnerCorrect InnerData
    // 同样,解引用 unmarshaledCorrect.Json 来获取 []byte
    if unmarshaledCorrect.Json != nil {
        err = json.Unmarshal(*unmarshaledCorrect.Json, &reParsedInnerCorrect) // 现在可以正常工作
        if err != nil {
            fmt.Printf("尝试解封d.Json到InnerData失败: %s\n", err.Error())
        } else {
            fmt.Printf("正确解封结果: %+v\n", reParsed
登录后复制

以上就是Go语言中json.RawMessage的正确使用:避免嵌套JSON解码陷阱的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习
PHP中文网抖音号
发现有趣的

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号