JSON(JavaScript 对象表示法)是一种简单的数据交换格式。从语法上讲,它类似于 JavaScript 的对象和列表。它最常用于 Web 后端和浏览器中运行的 JavaScript 程序之间的通信,但它也用于许多其他地方。它的主页 json.org 提供了该标准的非常清晰和简洁的定义。 使用 json package 包,可以轻松地从 Go 程序中读取和写入 JSON 数据。
Encoding
为了对 JSON 数据进行编码,我们使用 Marshal 函数。
func Marshal(v interface{}) ([]byte, error)
给定 Go 数据结构 Message,
type Message struct {
Name string
Body string
Time int64
}
和一个 Message 实例
m := Message{"Alice", "Hello", 1294706395881547000}
我们可以使用 json.Marshal 编组 m 的 JSON 编码版本:
b, err := json.Marshal(m)
如果一切顺利,err 将为 nil,b 将是包含以下 JSON 数据的 []byte:
b == []byte(`{"Name":"Alice","Body":"Hello","Time":1294706395881547000}`)
只有可以表示为有效 JSON 的数据结构才会被编码:
- JSON 对象仅支持字符串作为键;要编码 Go 映射类型,它必须采用 map[string]T 的形式(其中 T 是 json 包支持的任何 Go 类型)。
- Channel, 复数和函数类型无法编码。
- 不支持循环数据结构;它们会导致 Marshal 进入无限循环。
- 指针将被编码为它们指向的值(如果指针为 nil,则为“null”)。
json 包仅访问结构类型的导出字段(以大写字母开头的字段)。因此,只有结构体的导出字段才会出现在 JSON 输出中。
举一个循环引用的例子
"Cyclic data structures" 指的是循环引用的数据结构,即某个数据结构包含对自身的引用。在 JSON 编码中,循环引用可能导致无限循环,因此 json.Marshal 会检测并避免处理这种情况。
以下是一个简单的例子,演示了一个循环引用的数据结构:
package main
import (
"fmt"
"encoding/json"
)
type Employee struct {
Name string
Manager *Employee // 对自身的引用
}
func main() {
// 创建两个雇员实例,并建立循环引用
alice := Employee{Name: "Alice"}
bob := Employee{Name: "Bob", Manager: &alice}
alice.Manager = &bob
// 尝试编码包含循环引用的数据结构
_, err := json.Marshal(alice)
// 输出错误信息
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("No error. This won't be reached.")
}
}
在这个例子中,Employee
结构体有一个 Manager
字段,该字段是对 Employee
自身的引用,形成了一个循环引用。当我们尝试使用 json.Marshal
编码包含循环引用的数据结构时,会产生一个错误。
Decoding
为了解码 JSON 数据,我们使用 Unmarshal 函数。
func Unmarshal(data []byte, v interface{}) error
我们必须首先创建一个存储解码数据的地方
var m Message
并调用 json.Unmarshal,向其传递 JSON 数据的 []byte 和指向 m 的指针
err := json.Unmarshal(b, &m)
如果 b 包含适合 m 的有效 JSON,则调用后 err 将为 nil,并且来自 b 的数据将存储在结构 m 中,就像通过如下赋值一样:
m = Message{
Name: "Alice",
Body: "Hello",
Time: 1294706395881547000,
}
Unmarshal 如何识别存储解码数据的字段?对于给定的 JSON 键“Foo”,Unmarshal 将查找目标结构体的字段以查找(按优先顺序):
- 带有“Foo”标签的导出字段(有关结构标签的更多信息,请参阅 Go 规范 https://go.dev/ref/spec#Struct_types,通过反射获取标签的值。),
- 名为“Foo”的导出字段,或
- 名为“FOO”或“FoO”的导出字段或“Foo”的其他不区分大小写的匹配项。
当 JSON 数据的结构与 Go 类型不完全匹配时会发生什么?
b := []byte(`{"Name":"Bob","Food":"Pickle"}`)
var m Message
err := json.Unmarshal(b, &m)
Unmarshal 将仅解码它可以在目标类型中找到的字段。在这种情况下,仅填充 m 的 Name 字段,而 Food 字段将被忽略。当您希望从大型 JSON blob 中仅选择几个特定字段时,此行为特别有用。这也意味着目标结构中任何未导出的字段都不会受到 Unmarshal 的影响。
举例
type Person struct {
Name string json:"name"
AGe int
Money int
}
func main() {
// JSON 数据
jsonData := []byte({"name":"Alice","age":30,"food":"pizza"})
// 创建一个 Person 实例
var p Person
// 反序列化 JSON 到 Person 实例
err := json.Unmarshal(jsonData, &p)
// 输出结果
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Printf("Name: %s\n", p.Name) // 输出 Name 字段
fmt.Printf("Age: %d\n", p.AGe) // 输出 Age 字段
fmt.Printf("Sex: %d\n", p.Money)
}
}
输出结果:
Name: Alice
Age: 30
Sex: 0
但是如果事先不知道 JSON 数据的结构怎么办?
Generic JSON with interface
interface{}(空接口)类型描述了具有零个方法的接口。每个 Go 类型至少实现零个方法,因此满足空接口。
空接口作为通用容器类型:
var i interface{}
i = "a string"
i = 2011
i = 2.777
类型断言访问底层的具体类型:
r := i.(float64)
fmt.Println("the circle's area", math.Pi*r*r)
或者,如果基础类型未知,则类型开关确定类型:
switch v := i.(type) {
case int:
fmt.Println("twice i is", v*2)
case float64:
fmt.Println("the reciprocal of i is", 1/v)
case string:
h := len(v) / 2
fmt.Println("i swapped by halves is", v[h:]+v[:h])
default:
// i isn't one of the types above
}
json 包使用 map[string]interface{}和[]interface{}值来存储任意 JSON 对象和数组;它会很乐意将任何有效的 JSON blob 解组为简单的 interface{} 值。默认的具体 Go 类型是:
bool
for JSON booleans,float64
for JSON numbers,string
for JSON strings,nil
for JSON null.
Decoding arbitrary data 解码任意数据
考虑存储在变量 b 中的 JSON 数据:
b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)
在不知道该数据结构的情况下,我们可以使用 Unmarshal 将其解码为 interface{} 值:
var f interface{}
err := json.Unmarshal(b, &f)
此时 f 中的 Go 值将是一个映射,其键是字符串,其值本身存储为空接口值:
f = map[string]interface{}{
"Name": "Wednesday",
"Age": 6,
"Parents": []interface{}{
"Gomez",
"Morticia",
},
}
要访问此数据,我们可以使用类型断言来访问 f 的底层 map[string]interface{}:
m := f.(map[string]interface{})
然后,我们可以使用 range 语句迭代映射,并使用类型开关来访问其值作为具体类型:
for k, v := range m {
switch vv := v.(type) {
case string:
fmt.Println(k, "is string", vv)
case float64:
fmt.Println(k, "is float64", vv)
case []interface{}:
fmt.Println(k, "is an array:")
for i, u := range vv {
fmt.Println(i, u)
}
default:
fmt.Println(k, "is of a type I don't know how to handle")
}
}
通过这种方式,您可以使用未知的 JSON 数据,同时仍然享受类型安全的好处。
Reference Types
让我们定义一个 Go 类型来包含上一个示例中的数据:
type FamilyMember struct {
Name string
Age int
Parents []string
}
var m FamilyMember
err := json.Unmarshal(b, &m)
将数据解组为 FamilyMember 值可以按预期工作,但如果我们仔细观察,我们会发现发生了一件了不起的事情。使用 var 语句,我们分配了一个 FamilyMember 结构,然后向 Unmarshal 提供了指向该值的指针,但当时 Parent 字段是一个 nil 切片值。为了填充“Parents”字段,Unmarshal 在幕后分配了一个新切片。这是 Unmarshal 处理受支持的引用类型(指针、切片和映射)的典型方式。
这段描述涉及到 Go 中 JSON 反序列化时对引用类型的处理。在这里,我们定义了一个名为 FamilyMember
的结构体,其中包含一个切片字段 Parents
。
接下来,我们使用 json.Unmarshal
将 JSON 数据解码到 FamilyMember
类型的变量 m
中。在这个过程中,我们传递给 Unmarshal
的是变量 m
的指针(&m
),这是因为 JSON 解码时需要知道解码后的数据应该存放在哪个变量中。
在 FamilyMember
结构体中,有一个切片字段 Parents
。在 JSON 数据中,如果该字段是一个空数组(例如 []
),那么在 Go 中对应的是一个 nil
切片。当我们传递 &m
这个指针给 Unmarshal
时,它会发现 Parents
字段是一个 nil
切片,并且会在解码的过程中为该字段分配一个新的切片。
这里是一个简单的例子:
package main
import (
"encoding/json"
"fmt"
)
type FamilyMember struct {
Name string
Age int
Parents []string
}
func main() {
// JSON 数据
jsonData := []byte(`{"Name":"Alice","Age":30,"Parents":["John","Jane"]}`)
// 创建一个 FamilyMember 实例
var m FamilyMember
// 反序列化 JSON 到 FamilyMember 实例
err := json.Unmarshal(jsonData, &m)
// 输出结果
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Printf("Name: %s\n", m.Name)
fmt.Printf("Age: %d\n", m.Age)
fmt.Printf("Parents: %v\n", m.Parents)
}
}
在这个例子中,JSON 数据中的 Parents
字段是一个包含两个元素的数组。在 FamilyMember
结构体中,Parents
字段的初始值是 nil
切片。通过 json.Unmarshal
,我们成功地将 JSON 数据解码到了 FamilyMember
实例中,并且 Parents
字段被填充为 ["John","Jane"]
。这是因为在解码的过程中,Unmarshal
识别到 Parents
字段是一个 nil
切片,于是分配了一个新的切片并填充了数据。
考虑解组到此数据结构中:
type Foo struct {
Bar *Bar
}
如果 JSON 对象中有 Bar 字段,Unmarshal 将分配一个新的 Bar 并填充它。如果不是,Bar 将保留为 nil 指针。
由此产生了一个有用的模式:如果您有一个接收几种不同消息类型的应用程序,您可以定义“接收者”结构,例如
type IncomingMessage struct {
Cmd *Command
Msg *Message
}
发送方可以填充顶级 JSON 对象的 Cmd 字段和/或 Msg 字段,具体取决于他们想要通信的消息类型。当将 JSON 解码为 IncomingMessage 结构时,Unmarshal 将仅分配 JSON 数据中存在的数据结构。要知道要处理哪些消息,程序员只需测试 Cmd 或 Msg 是否不为零。
Streaming Encoders and Decoders
json 包提供了 Decoder 和 Encoder 类型来支持读取和写入 JSON 数据流的常见操作。 NewDecoder 和 NewEncoder 函数包装了 io.Reader 和 io.Writer 接口类型。
func NewDecoder(r io.Reader) *Decoder
func NewEncoder(w io.Writer) *Encoder
下面是一个示例程序,它从标准输入读取一系列 JSON 对象,从每个对象中删除除 Name 字段之外的所有对象,然后将对象写入标准输出:
package main
import (
"encoding/json"
"log"
"os"
)
func main() {
dec := json.NewDecoder(os.Stdin)
enc := json.NewEncoder(os.Stdout)
for {
var v map[string]interface{}
if err := dec.Decode(&v); err != nil {
log.Println(err)
return
}
for k := range v {
if k != "Name" {
delete(v, k)
}
}
if err := enc.Encode(&v); err != nil {
log.Println(err)
}
}
}
由于 Readers 和 Writers 无处不在,这些 Encoder 和 Decoder 类型可用于广泛的场景,例如读取和写入 HTTP 连接、WebSocket 或文件。
References
欲了解更多信息,请参阅 json package documentation. 有关 json 的示例用法,请参阅以下源文件 jsonrpc package. https://go.dev/blog/json
文档信息
- 本文作者:Zzhiter
- 本文链接:http://zzhiter.top/2024/01/19/Dive-into-Go-native-json-package/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)