Go JSON序列化与反序列化
Go 语言中的 JSON 基础
在现代软件开发中,JSON(JavaScript Object Notation)已经成为一种广泛使用的数据交换格式。它以其简洁、易读的文本格式,在不同语言编写的系统之间轻松传递数据。Go 语言对 JSON 处理提供了强大且高效的支持,无论是将 Go 数据结构转换为 JSON 格式(序列化),还是将 JSON 数据解析回 Go 数据结构(反序列化)。
JSON 格式简介
JSON 数据主要由两种结构组成:对象和数组。对象是一个无序的键值对集合,键必须是字符串,值可以是字符串、数字、布尔值、null、对象或数组。数组是一个有序的值序列,值同样可以是上述提到的任意类型。例如,以下是一个简单的 JSON 对象:
{
"name": "John",
"age": 30,
"isStudent": false,
"hobbies": ["reading", "swimming"],
"address": {
"city": "New York",
"country": "USA"
}
}
在 Go 语言中,我们需要将这样的 JSON 数据映射到相应的结构体或其他数据类型,以便于处理。
Go 语言的 JSON 序列化
使用 encoding/json
包
Go 语言的标准库 encoding/json
包提供了用于 JSON 序列化和反序列化的函数。序列化是将 Go 数据结构转换为 JSON 格式的过程。最常用的函数是 json.Marshal
,它接受一个 Go 值作为参数,并返回一个字节切片,其中包含该值的 JSON 编码。
基本类型的序列化
对于基本类型,如字符串、整数、布尔值等,序列化非常直接。
package main
import (
"encoding/json"
"fmt"
)
func main() {
var str string = "Hello, JSON"
data, err := json.Marshal(str)
if err != nil {
fmt.Println("Marshal error:", err)
return
}
fmt.Println(string(data))
}
在上述代码中,我们定义了一个字符串变量 str
,然后使用 json.Marshal
对其进行序列化。如果序列化成功,data
将包含 JSON 格式的字节切片,我们将其转换回字符串并打印。这里,输出将是 "Hello, JSON"
,注意字符串两边的双引号是 JSON 格式要求的。
结构体的序列化
结构体是 Go 语言中常用的数据结构,将结构体序列化为 JSON 是非常常见的需求。
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
IsStudent bool `json:"is_student"`
}
func main() {
p := Person{
Name: "Alice",
Age: 25,
IsStudent: true,
}
data, err := json.Marshal(p)
if err != nil {
fmt.Println("Marshal error:", err)
return
}
fmt.Println(string(data))
}
在这段代码中,我们定义了一个 Person
结构体。结构体字段后面的反引号中的内容是结构体标签(struct tag)。这里的 json:"name"
、json:"age"
和 json:"is_student"
标签用于指定在 JSON 序列化时字段的名称。如果没有这些标签,默认会使用结构体字段的大写名称,但 JSON 标准中通常使用小写字母和下划线命名。运行这段代码,输出将是 {"name":"Alice","age":25,"is_student":true}
。
嵌套结构体的序列化
当结构体中包含其他结构体作为字段时,同样可以轻松序列化。
package main
import (
"encoding/json"
"fmt"
)
type Address struct {
City string `json:"city"`
Country string `json:"country"`
}
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Address Address `json:"address"`
}
func main() {
a := Address{
City: "Shanghai",
Country: "China",
}
p := Person{
Name: "Bob",
Age: 32,
Address: a,
}
data, err := json.Marshal(p)
if err != nil {
fmt.Println("Marshal error:", err)
return
}
fmt.Println(string(data))
}
这里 Person
结构体包含一个 Address
结构体类型的字段。序列化后的 JSON 数据将反映这种嵌套结构,输出为 {"name":"Bob","age":32,"address":{"city":"Shanghai","country":"China"}}
。
切片和映射的序列化
切片和映射在 Go 语言中也很常用,它们同样可以被序列化为 JSON。
package main
import (
"encoding/json"
"fmt"
)
func main() {
sliceData := []string{"apple", "banana", "cherry"}
sliceBytes, err := json.Marshal(sliceData)
if err != nil {
fmt.Println("Marshal error:", err)
return
}
fmt.Println(string(sliceBytes))
mapData := map[string]int{"one": 1, "two": 2, "three": 3}
mapBytes, err := json.Marshal(mapData)
if err != nil {
fmt.Println("Marshal error:", err)
return
}
fmt.Println(string(mapBytes))
}
切片会被序列化为 JSON 数组,上述代码中 sliceData
序列化后的输出是 ["apple","banana","cherry"]
。映射会被序列化为 JSON 对象,mapData
序列化后的输出是 {"one":1,"two":2,"three":3}
。
格式化输出
json.Marshal
生成的是紧凑格式的 JSON 数据,不包含空格和换行符,这在需要人类可读的场景下不太友好。encoding/json
包提供了 json.MarshalIndent
函数来生成格式化后的 JSON 数据。
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
IsStudent bool `json:"is_student"`
}
func main() {
p := Person{
Name: "Charlie",
Age: 28,
IsStudent: false,
}
data, err := json.MarshalIndent(p, "", " ")
if err != nil {
fmt.Println("Marshal error:", err)
return
}
fmt.Println(string(data))
}
json.MarshalIndent
的第二个参数是前缀,第三个参数是缩进字符串。在上述代码中,我们使用两个空格作为缩进,输出的 JSON 数据将是:
{
"name": "Charlie",
"age": 28,
"is_student": false
}
Go 语言的 JSON 反序列化
使用 json.Unmarshal
反序列化是将 JSON 数据转换回 Go 数据结构的过程。encoding/json
包中的 json.Unmarshal
函数用于此目的。它接受两个参数,第一个是包含 JSON 数据的字节切片,第二个是指向目标 Go 值的指针。
基本类型的反序列化
对于基本类型的反序列化,要注意 JSON 数据类型和 Go 语言数据类型的匹配。
package main
import (
"encoding/json"
"fmt"
)
func main() {
jsonStr := `"Hello from JSON"`
var str string
err := json.Unmarshal([]byte(jsonStr), &str)
if err != nil {
fmt.Println("Unmarshal error:", err)
return
}
fmt.Println(str)
}
这里我们将一个 JSON 字符串反序列化为 Go 字符串。注意,json.Unmarshal
的第一个参数必须是字节切片,所以我们使用 []byte(jsonStr)
进行转换。同时,第二个参数必须是指针,这样函数才能修改目标值。
结构体的反序列化
将 JSON 数据反序列化为结构体是常见的操作。
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
IsStudent bool `json:"is_student"`
}
func main() {
jsonStr := `{"name":"David","age":35,"is_student":false}`
var p Person
err := json.Unmarshal([]byte(jsonStr), &p)
if err != nil {
fmt.Println("Unmarshal error:", err)
return
}
fmt.Printf("Name: %s, Age: %d, IsStudent: %v\n", p.Name, p.Age, p.IsStudent)
}
在这个例子中,我们定义了 Person
结构体,并将 JSON 字符串反序列化为 Person
结构体实例。结构体标签在这里同样重要,它帮助 json.Unmarshal
正确地将 JSON 字段映射到结构体字段。
嵌套结构体的反序列化
处理嵌套结构体的反序列化时,只要结构体定义正确,过程与普通结构体类似。
package main
import (
"encoding/json"
"fmt"
)
type Address struct {
City string `json:"city"`
Country string `json:"country"`
}
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Address Address `json:"address"`
}
func main() {
jsonStr := `{"name":"Eva","age":22,"address":{"city":"Paris","country":"France"}}`
var p Person
err := json.Unmarshal([]byte(jsonStr), &p)
if err != nil {
fmt.Println("Unmarshal error:", err)
return
}
fmt.Printf("Name: %s, Age: %d, City: %s, Country: %s\n", p.Name, p.Age, p.Address.City, p.Address.Country)
}
这里 Person
结构体包含 Address
结构体,JSON 数据中的嵌套结构也能正确地反序列化为对应的 Go 结构体。
切片和映射的反序列化
反序列化 JSON 数组到 Go 切片以及 JSON 对象到 Go 映射也是常见的操作。
package main
import (
"encoding/json"
"fmt"
)
func main() {
jsonArray := `["red", "green", "blue"]`
var sliceData []string
err := json.Unmarshal([]byte(jsonArray), &sliceData)
if err != nil {
fmt.Println("Unmarshal error:", err)
return
}
fmt.Println(sliceData)
jsonObject := `{"one": 1, "two": 2, "three": 3}`
var mapData map[string]int
err = json.Unmarshal([]byte(jsonObject), &mapData)
if err != nil {
fmt.Println("Unmarshal error:", err)
return
}
fmt.Println(mapData)
}
在这个例子中,我们将 JSON 数组反序列化为字符串切片,将 JSON 对象反序列化为字符串到整数的映射。
处理 JSON 中的可选字段
在 JSON 数据中,某些字段可能是可选的,即数据中可能不存在这些字段。在 Go 结构体反序列化时,可以通过设置结构体字段的默认值和使用指针类型来处理这种情况。
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Email *string `json:"email,omitempty"`
}
func main() {
jsonStr1 := `{"name":"Frank","age":27}`
var p1 Person
err := json.Unmarshal([]byte(jsonStr1), &p1)
if err != nil {
fmt.Println("Unmarshal error:", err)
return
}
fmt.Printf("Name: %s, Age: %d, Email: %v\n", p1.Name, p1.Age, p1.Email)
jsonStr2 := `{"name":"Grace","age":31,"email":"grace@example.com"}`
var p2 Person
err = json.Unmarshal([]byte(jsonStr2), &p2)
if err != nil {
fmt.Println("Unmarshal error:", err)
return
}
fmt.Printf("Name: %s, Age: %d, Email: %v\n", p2.Name, p2.Age, p2.Email)
}
在 Person
结构体中,Email
字段是指针类型,并且标签中使用了 omitempty
。当 JSON 数据中不存在 email
字段时,p1.Email
将为 nil
;当存在时,p2.Email
将指向反序列化后的字符串值。
高级 JSON 处理技巧
自定义 Marshal 和 Unmarshal 方法
有时候,默认的序列化和反序列化行为不能满足需求,我们可以为结构体定义自定义的 MarshalJSON
和 UnmarshalJSON
方法。
package main
import (
"encoding/json"
"fmt"
"time"
)
type CustomTime struct {
time.Time
}
func (ct CustomTime) MarshalJSON() ([]byte, error) {
// 自定义时间格式
formated := ct.Time.Format("2006-01-02 15:04:05")
return json.Marshal(formated)
}
func (ct *CustomTime) UnmarshalJSON(data []byte) error {
var s string
err := json.Unmarshal(data, &s)
if err != nil {
return err
}
t, err := time.Parse("2006-01-02 15:04:05", s)
if err != nil {
return err
}
ct.Time = t
return nil
}
func main() {
now := CustomTime{time.Now()}
data, err := json.Marshal(now)
if err != nil {
fmt.Println("Marshal error:", err)
return
}
fmt.Println(string(data))
var ct CustomTime
err = json.Unmarshal([]byte(`"2023-10-01 12:34:56"`), &ct)
if err != nil {
fmt.Println("Unmarshal error:", err)
return
}
fmt.Println(ct.Time)
}
在这个例子中,我们定义了 CustomTime
结构体,它嵌入了 time.Time
。通过实现 MarshalJSON
和 UnmarshalJSON
方法,我们可以自定义时间的 JSON 序列化和反序列化格式。
使用 json.RawMessage
json.RawMessage
类型允许在结构体中存储未解析的 JSON 数据。这在需要延迟解析部分 JSON 数据或者处理结构不确定的 JSON 数据时非常有用。
package main
import (
"encoding/json"
"fmt"
)
type Container struct {
Field1 string `json:"field1"`
Field2 json.RawMessage `json:"field2"`
}
func main() {
jsonStr := `{"field1":"value1","field2":{"subfield":"subvalue"}}`
var c Container
err := json.Unmarshal([]byte(jsonStr), &c)
if err != nil {
fmt.Println("Unmarshal error:", err)
return
}
fmt.Println("Field1:", c.Field1)
fmt.Println("Field2:", string(c.Field2))
var subData map[string]string
err = json.Unmarshal(c.Field2, &subData)
if err != nil {
fmt.Println("Unmarshal sub data error:", err)
return
}
fmt.Println("Subfield:", subData["subfield"])
}
在 Container
结构体中,Field2
是 json.RawMessage
类型。首先,我们将 JSON 数据反序列化为 Container
结构体,Field2
中的 JSON 数据被存储为字节切片。然后,我们可以根据需要进一步将 Field2
反序列化为具体的 Go 数据结构。
处理 JSON 流
在处理大量 JSON 数据或者需要逐块处理 JSON 数据时,可以使用 json.Decoder
和 json.Encoder
来处理 JSON 流。
package main
import (
"encoding/json"
"fmt"
"strings"
)
func main() {
jsonStream := strings.NewReader(`{"name":"Hank","age":40} {"name":"Ivy","age":29}`)
decoder := json.NewDecoder(jsonStream)
for {
var person map[string]interface{}
err := decoder.Decode(&person)
if err != nil {
break
}
fmt.Println(person)
}
}
这里我们使用 strings.NewReader
创建一个模拟的 JSON 数据流。json.NewDecoder
从这个流中读取数据,并逐块将其反序列化为 map[string]interface{}
。在实际应用中,jsonStream
可能来自网络连接、文件等数据源。
处理 JSON 中的特殊值
JSON 中有一些特殊值,如 null
。在 Go 语言中,处理 null
值需要特别注意。例如,在反序列化时,如果 JSON 中的某个字段是 null
,而对应的 Go 结构体字段是基本类型,会导致反序列化错误。可以使用指针类型或者 interface{}
类型来处理 null
值。
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Name string `json:"name"`
Age *int `json:"age"`
Details interface{} `json:"details"`
}
func main() {
jsonStr := `{"name":"Jack","age":null,"details":{"note":"Some note"}}`
var p Person
err := json.Unmarshal([]byte(jsonStr), &p)
if err != nil {
fmt.Println("Unmarshal error:", err)
return
}
fmt.Printf("Name: %s, Age: %v, Details: %v\n", p.Name, p.Age, p.Details)
}
在这个例子中,Age
字段是指针类型,当 JSON 中的 age
为 null
时,p.Age
将为 nil
。Details
字段使用 interface{}
类型,可以存储任何 JSON 值,包括 null
。
性能优化与注意事项
性能优化
- 预分配内存:在序列化和反序列化切片和映射时,预分配足够的内存可以减少内存重新分配的次数,提高性能。例如,在反序列化一个已知长度的 JSON 数组到切片时,可以预先分配切片的容量。
package main
import (
"encoding/json"
"fmt"
)
func main() {
jsonArray := `["item1","item2","item3"]`
var sliceData []string
// 预先分配容量
sliceData = make([]string, 0, 3)
err := json.Unmarshal([]byte(jsonArray), &sliceData)
if err != nil {
fmt.Println("Unmarshal error:", err)
return
}
fmt.Println(sliceData)
}
- 减少反射使用:虽然 Go 语言的
encoding/json
包内部使用反射来处理结构体的序列化和反序列化,但在可能的情况下,可以通过自定义MarshalJSON
和UnmarshalJSON
方法来减少反射的使用。反射操作相对较慢,直接处理数据可以提高性能。
注意事项
- 字段命名和标签:在结构体序列化和反序列化时,字段命名和结构体标签非常重要。确保 JSON 字段名和结构体字段标签一致,否则可能导致数据映射错误。同时,注意标签中的
omitempty
等选项的使用,避免意外的数据丢失或错误。 - 数据类型匹配:在反序列化时,要确保 JSON 数据类型和目标 Go 数据类型匹配。例如,不能将 JSON 字符串反序列化为整数类型,否则会导致反序列化错误。在处理数值类型时,还要注意 JSON 中的数值范围和 Go 语言对应类型的范围是否匹配。
- 错误处理:在序列化和反序列化过程中,始终要进行错误处理。
json.Marshal
和json.Unmarshal
等函数可能会返回错误,如 JSON 格式错误、数据类型不匹配等。及时处理这些错误可以避免程序出现意外行为。
通过深入理解和掌握 Go 语言中 JSON 序列化与反序列化的原理、方法以及高级技巧,开发者可以更加高效地处理 JSON 数据,构建稳定、高性能的应用程序。无论是在 Web 开发、微服务架构还是数据处理等领域,这些知识都具有重要的实用价值。