这篇文章详细讲解了 Go 语言中 JSON 序列化和反序列化操作的各种用法,包括基本用法、通用 JSON 处理、类型关系、自定义时间格式、匿名结构体处理 JSON、使用 json.RawMessage 处理 []byte 字段以及如何控制 HTML 特殊字符的转义。文章还包含大量代码示例和详细解释,帮助读者理解每个操作的具体实现和原理。
JSON(JavaScript Object Notation)是一种简单的数据交换格式。从语法上讲,它类似于 JavaScript 的对象和列表。它最常用于 Web 后端和在浏览器中运行的 JavaScript 程序之间的通信,但它也用于许多其他地方。它的主页 <json.org> 提供了非常清晰和简洁的标准定义。
使用 json 包,可以轻而易举地从 Go 程序中读写 JSON 数据。
Golang JSON 基础用法
以下是 Golang 中 JSON 序列化(Marshal)和反序列化(Unmarshal)的基础用法示例:
package main
import (
"encoding/json"
"fmt"
)
// 给定 Go 数据结构 Message
type Message struct {
Name string
Body string
Time int64
}
func main() {
// 序列化
m := Message{"Alice", "Hello", 1294706395881547000}
b, err := json.Marshal(m)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("b:%q type:%T\n", string(b), b)
// 反序列化
var m1 Message
if err := json.Unmarshal(b, &m1); err != nil {
fmt.Println(err)
return
}
fmt.Printf("m1:%+v\n", m1)
}
运行示例,输出结果:
b:"{\"Name\":\"Alice\",\"Body\":\"Hello\",\"Time\":1294706395881547000}" type:[]uint8
m1:{Name:Alice Body:Hello Time:1294706395881547000}
Golang JSON 编码技巧
我们使用 Marshal
函数对 JSON 数据进行编码(序列化):
func Marshal(v interface{}) ([]byte, error)
只有可以表示为有效 JSON 的数据结构才会被编码:
- JSON 对象只支持字符串作为键;要对 Go 的 map 类型进行编码,它必须采用
map[string]T
形式(其中T
是 json 包支持的任何 Go 类型)。 - 无法对
channel
、complex
和function
类型进行编码。 - 不支持循环数据结构;它们会使
Marshal
进入无限循环。 - 指针将被编码为它们指向的值(如果指针为
nil
,则为null
)。
json 包只访问结构体类型的导出字段(以大写字母开头的字段)。因此,只有结构体的导出字段才会出现在 JSON 输出中。
要解码(反序列化) JSON 数据,我们使用 Unmarshal
函数:
func Unmarshal(data []byte, v interface{}) error
Golang JSON 解码最佳实践:如何处理类型不匹配?
Unmarshal
如何识别存储解码数据的字段?对于一个给定的 JSON 键 Foo
,Unmarshal
将遍历目标结构体的字段,以找到(按以下优先级顺序):
- 带有
Foo
标签的导出字段, - 名为
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
的影响。
但是,如果您事先不知道 JSON 数据的结构怎么办?
如何使用 Golang 接口处理通用 JSON?
interface{}
(空接口)类型描述了一个具有零方法的接口。每个 Go 类型都至少实现零个方法,因此满足空接口。
空接口用作通用容器类型:
var i interface{}
i = "a string"
i = 2023
i = 2.777
类型断言访问底层的具体类型:
r := i.(float64)
fmt.Println("the circle's area", math.Pi*r*r)
或者,如果底层类型未知,则类型 switch
确定类型:
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{}
值。
Golang JSON 类型关系详解
JSON 和 Go 类型不是一对一匹配的。下表描述了编码和解码时的类型关系。
Go 类型 | JSON 类型 |
---|---|
bool | boolean |
float64 | number |
string | string |
nil | null |
time.Time | RFC 3339 时间字符串 |
数组或切片(除 []byte 外) | 数组 |
您会注意到缺少 float32
和 int
类型。别担心,您当然可以将数字编码和解码为这些类型,它们只是在 JSON 规范中没有明确的类型。例如,如果你在 JSON 中编码一个整数,它保证没有小数点。但是,如果有人在您解码之前将该 JSON 值转换为浮点数,您将收到运行时错误。
序列化 JSON 数据时很少会遇到错误,但反序列化 JSON 经常会导致错误。以下是一些需要注意的事项:
- 任何类型冲突都会导致错误。例如,您不能将字符串解码为 int,即使字符串化数字:
"speed": "42"
- 浮点数无法解码为整数
null
不能被解码为没有nil
选项的值。例如,如果您有一个可以为空的数字字段,您应该解码为*int
time.Time
默认只能解码 RFC 3339 字符串 - 其他类型的时间将失败- 数组和切片值编码为 JSON 数组,除了
[]byte
编码为 base64 编码的字符串
Golang 解码任意 JSON 数据:实战演练
下面这个存储在变量 b
中的 JSON 数据:
b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)
在不知道此数据结构的情况下,我们可以使用 Unmarshal
将其解码为 interface{}
值:
var f interface{}
err := json.Unmarshal(b, &f)
此时,f
中的 Go 值将是一个 map
,其键为字符串,其值存储为自身的空接口值:
f = map[string]interface{}{
"Name": "Wednesday",
"Age": 6,
"Parents": []interface{}{
"Gomez",
"Morticia",
},
}
要访问此数据,我们可以使用类型断言来访问 f
的底层 map[string]interface{}
:
m := f.(map[string]interface{})
然后我们可以使用 range
语句遍历 map 并使用类型 switch 来访问其值作为它们的具体类型:
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 数据,同时仍然享受类型安全的好处。
Golang JSON 引用类型:指针、切片和映射
让我们定义一个 Go 类型来包含上一个示例中的数据:
type FamilyMember struct {
Name string
Age int
Parents []string
}
var m FamilyMember
err := json.Unmarshal(b, &m)
将该数据解码为 FamilyMember
值按预期工作,但如果我们仔细观察,我们会发现发生了一件了不起的事情。通过 var
语句,我们分配了一个 FamilyMember
结构,然后将指向该值的指针提供给 Unmarshal
,但此时 Parents
字段是一个 nil
切片值。为了填充 Parents
字段,Unmarshal
在幕后分配了一个新切片。这是 Unmarshal
使用支持的引用类型(指针、切片和映射)的典型方式。
考虑解组到这个数据结构中:
type Foo struct {
Bar *Bar
}
如果 JSON 对象中有一个 Bar
字段,Unmarshal
将分配一个新的 Bar
并填充它。否则,Bar
将保留为 nil
指针。
Golang JSON 流编码器和解码器:高效处理 JSON 数据流
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 或文件。
Golang 如何读写 JSON 文件?
经常我们会使用 JSON 文件来存储配置。 Go 使读取和写入 JSON 文件变得容易。
在 Go 中将 JSON 写入文件:
type car struct {
Speed int `json:"speed"`
Make string `json:"make"`
}
c := car{
Speed: 10,
Make: "Tesla",
}
dat, err := json.Marshal(c)
if err != nil {
return err
}
err = os.WriteFile("/tmp/file.json", dat, 0644)
if err != nil {
return err
}
在 Go 中从文件中读取 JSON:
type car struct {
Speed int `json:"speed"`
Make string `json:"make"`
}
dat, err := os.ReadFile("/tmp/file.json")
if err != nil {
return err
}
c := car{}
err = json.Unmarshal(dat, &c)
if err != nil {
return err
}
Golang 结构体 JSON 标签:掌握 JSON 序列化和反序列化的关键
Go struct 标签是出现在 Go struct 声明中类型之后的由反引号(`
)字符包裹的注解。json 标签主要在 struct 与 JSON 数据转换的过程中使用。
Golang 使用 JSON 标签指定键名
type User struct {
FirstName string `json:"first_name"` // JSON 键为 "first_name"
BirthYear int `json:"birth_year"` // JSON 键为 "birth_year"
Email string // JSON 键为 "Email"
}
Golang 设置 JSON 忽略空值字段:omitempty
当 struct 中的字段没有值时, json.Marshal()
时候默认输出没有值的字段的类型零值(例如 int
和 float
类型零值是 0
, string
类型零值是 ""
,对象类型零值是 nil
)。
标签选项添加 omitempty
可以忽略空值字段的序列化:
type User struct {
FirstName string `json:"first_name,omitempty"`
BirthYear int `json:"birth_year"`
}
// 如果 FirstName = "" ,BirthYear = 0
// json.Marshal 结果将是:
// {"birth_year":0}
// 如果 FirstName = "axiaoxin" and BirthYear = 0
// json.Marshal 结果将是:
// {"first_name":"axiaoxin","birth_year":0}
嵌套的结构体没有空值:
type dimension struct {
Height int
Width int
}
type Dog struct {
Breed string
WeightKg int
Size dimension `json:",omitempty"`
}
func main() {
d := Dog{
Breed: "pug",
}
b, _ := json.Marshal(d)
fmt.Println(string(b))
}
运行示例,输出结果:
{"Breed":"pug","WeightKg":0,"Size":{"Height":0,"Width":0}}
在这种情况下,即使我们 Size 属性设置了它的 omitempty
标签也未对他赋值,它仍然会出现在输出中。这是因为结构体在 Go 中没有空值。要解决这个问题,请改用结构体指针:
type Dog struct {
Breed string
WeightKg int
// 现在 `Size` 是指向 `dimension` 实例的指针
Size *dimension `json:",omitempty"`
}
尽管结构体没有“空”值,但结构体指针有,空值为 nil
。Size 声明为指针类型后,运行结果符合我们的预期:
{ "Breed": "pug", "WeightKg": 0 }
使用指针还能让我们能够支持区分默认值和零值,例如,如果我们有一个描述餐厅的结构体,将就座顾客的数量作为属性:
type Restaurant struct {
NumberOfCustomers int `json:",omitempty"`
}
func main() {
d := Restaurant{
NumberOfCustomers: 0,
}
b, _ := json.Marshal(d)
fmt.Println(string(b))
}
运行代码将会输出:
{}
在这种情况下,餐厅的顾客数量实际上可以为 0
的,我们希望在 JSON 对象中传达此信息。同时,如果未设置 NumberOfCustomers
,我们希望省略这个 JSON 键。
换句话说,我们不希望 0 成为 NumberOfCustomers 的“空”值。
解决这个问题的一种方法是使用 int 指针:
type Restaurant struct {
NumberOfCustomers *int `json:",omitempty"`
}
现在空值是一个 nil 指针,它不同于 0 :
package main
import (
"encoding/json"
"fmt"
)
type Restaurant struct {
NumberOfCustomers *int `json:",omitempty"`
}
func main() {
d1 := Restaurant{}
b, _ := json.Marshal(d1)
fmt.Println(string(b))
//Prints: {}
n := 0
d2 := Restaurant{
NumberOfCustomers: &n,
}
b, _ = json.Marshal(d2)
fmt.Println(string(b))
//Prints: {"NumberOfCustomers":0}
}
Golang 设置 JSON 忽略指定字段:-
如上所述,json.Marshal
会忽略未导出(小写)字段。如果想忽略其他字段,可以使用 -
标签。
标签选项添加 -
可以忽略该字段的序列化:
type User struct {
FirstName string `json:"first_name"`
BirthYear int `json:"birth_year"`
Password string `json:"-"`
}
// json.Marshal 结果中将没有 Password 字段
Golang JSON 使用字符串传递数字:string
JSON 数据中可能会使用字符串类型的数字,在结构体标签中添加 string
来告诉 json 包以字符串形式中解析数字
type User struct {
FirstName string `json:"first_name"`
BirthYear int `json:"birth_year,string"`
}
func main() {
b := []byte(`{"first_name": "axiaoxin", "birth_year": "2023"}`)
var u User
if err := json.Unmarshal(b, &u); err != nil {
fmt.Println(err)
return
}
fmt.Printf("u:%+v\n", u)
// u:{FirstName:axiaoxin BirthYear:2023}
}
示例中 JSON b
中的 birth_year
是字符串类型,如果是 int 类型则会报错。
如何解决 Golang JSON 整数变科学计数法浮点数
JSON 中没有整型和浮点型之分,统称为 number。
默认情况下,如果是 interface{}
对应数字时,数字经过反序列化之后都会成为 float64
类型。
如果输入的数字比较大,interface{}
对应的数字都变成科学计数法表示的浮点数
type User struct {
FirstName string `json:"first_name"`
BirthYear int `json:"birth_year"`
}
func main() {
b := []byte(`{"first_name": "axiaoxin", "birth_year": 1234567}`)
m := map[string]interface{}{}
if err := json.Unmarshal(b, &m); err != nil {
fmt.Println(err)
return
}
fmt.Printf("m: %+v\n", m)
fmt.Printf("birth_year:%v type:%T", m["birth_year"], m["birth_year"])
}
运行示例,输出结果:
m: map[birth_year:1.234567e+06 first_name:axiaoxin]
birth_year:1.234567e+06 type:float64
解决方式:可以 UseNumber()
启用 json.Number
来表示数字
type User struct {
FirstName string `json:"first_name"`
BirthYear int `json:"birth_year"`
}
func main() {
b := []byte(`{"first_name": "axiaoxin", "birth_year": 1234567}`)
m := map[string]interface{}{}
decoder := json.NewDecoder(bytes.NewReader(b))
decoder.UseNumber()
decoder.Decode(&m)
fmt.Printf("m: %+v\n", m)
fmt.Printf("birth_year:%v type:%T\n", m["birth_year"], m["birth_year"])
}
运行示例,输出结果:
m: map[birth_year:1234567 first_name:axiaoxin]
birth_year:1234567 type:json.Number
json.Number
类型本质上是一个 string 类型,它实现了 Int64
和 Float64
方法,使用者可以按需转换。
Golang 如何自定义 JSON 中的 time.Time 格式?
json 包默认只能处理 RFC 3339 格式的时间字符串,这个字符串并非我们常用的 2006-01-02 15:04:05
这种格式。
如果某个类型实现了 MarshalJSON()([]byte, error)
和 UnmarshalJSON(b []byte) error
方法,那么这个类型在 json 序列化和反序列化时就会使用你自定义的方法。
要自定义 JSON 中时间格式,可以实现自己的序列化和反序列化方法:
type Order struct {
ID int `json:"id"`
Title string `json:"title"`
CreatedTime time.Time `json:"created_time"`
}
const layout = "2006-01-02 15:04:05"
// MarshalJSON 为Order类型实现自定义的MarshalJSON方法
func (o *Order) MarshalJSON() ([]byte, error) {
type TempOrder Order // 定义与Order字段一致的新类型
return json.Marshal(struct {
*TempOrder // 避免直接嵌套Order进入死循环
CreatedTime string `json:"created_time"`
}{
TempOrder: (*TempOrder)(o),
CreatedTime: o.CreatedTime.Format(layout), // 转为我们自定义的时间格式
})
}
// UnmarshalJSON 为Order类型实现自定义的UnmarshalJSON方法
func (o *Order) UnmarshalJSON(data []byte) error {
type TempOrder Order // 定义与Order字段一致的新类型
ot := struct {
*TempOrder // 避免直接嵌套Order进入死循环
CreatedTime string `json:"created_time"`
}{
TempOrder: (*TempOrder)(o),
}
if err := json.Unmarshal(data, &ot); err != nil {
return err
}
var err error
// 使用我们的自定义时间格式解析json 为 time.Time,并赋值给 CreatedTime 字段
o.CreatedTime, err = time.Parse(layout, ot.CreatedTime)
if err != nil {
return err
}
return nil
}
func main() {
o1 := Order{
ID: 123456,
Title: "《人言兑》",
CreatedTime: time.Now(),
}
// json.Marshal 将使用 Order 自己的 MarshalJSON 方法进行序列化
b, err := json.Marshal(&o1)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("b: %s\n", b)
// json.Unmarshal 将使用 Order 自己的 UnmarshalJSON 方法进行反序列化
jsonStr := `{"created_time":"2009-11-10 23:00:00","id":123456,"title":"《人言兑》"}`
var o2 Order
if err := json.Unmarshal([]byte(jsonStr), &o2); err != nil {
fmt.Println(err)
return
}
fmt.Printf("o2: %+v\n", o2)
}
运行示例,输出结果:
b: {"id":123456,"title":"《人言兑》","created_time":"2009-11-10 23:00:00"}
o2: {ID:123456 Title:《人言兑》 CreatedTime:2009-11-10 23:00:00 +0000 UTC}
使用 Golang 匿名结构体处理 JSON
使用 Golang 匿名结构体添加或删除 JSON 字段
通过在匿名结构体上添加或覆盖嵌套结构体的字段进行删除字段:
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Password string `json:"password"`
}
func main() {
user := User{
ID: 123456,
Name: "《人言兑》",
Password: "abc",
}
token := "kkk"
b, err := json.Marshal(struct { // 匿名结构体
*User
Password *struct{} `json:"password,omitempty"` // 使用 omitempty 覆盖删除嵌套结构体中的 password
Token string `json:"token"` // 添加 token 字段
}{
User: &user,
Token: token,
})
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(b))
}
注意匿名结构体中的 Password
字段类型为 *struct{}
运行示例,输出结果:
{"id":123456,"name":"《人言兑》","token":"kkk"}
使用 Golang 匿名结构体进行组合或拆分 JSON
将两个结构体组合输出 JSON:
type BlogPost struct {
URL string `json:"url"`
Title string `json:"title"`
}
type Analytics struct {
Visitors int `json:"visitors"`
PageViews int `json:"page_views"`
}
func main() {
post := &BlogPost{
URL: "https://blog.axiaoxin.com",
Title: "《人言兑》",
}
analytics := &Analytics{
Visitors: 666,
PageViews: 888,
}
b, err := json.Marshal(struct {
*BlogPost
*Analytics
}{
post,
analytics,
})
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(b))
}
运行示例,输出结果:
{"url":"https://blog.axiaoxin.com","title":"《人言兑》","visitors":666,"page_views":888}
将 JSON 拆分到两个结构体:
type BlogPost struct {
URL string `json:"url"`
Title string `json:"title"`
}
type Analytics struct {
Visitors int `json:"visitors"`
PageViews int `json:"page_views"`
}
func main() {
b := []byte(`{"url":"https://blog.axiaoxin.com","title":"《人言兑》","visitors":666,"page_views":888}`)
post := BlogPost{}
analytics := Analytics{}
err := json.Unmarshal(b, &struct {
*BlogPost
*Analytics
}{
&post,
&analytics,
})
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("post: %+v\nanalytics: %+v\n", post, analytics)
}
运行示例,输出结果:
post: {URL:https://blog.axiaoxin.com Title:《人言兑》}
analytics: {Visitors:666 PageViews:888}
Golang 使用 json.RawMessage
处理 []byte
JSON 字段变为 base64 编码的问题
如果 JSON 串没有固定的格式导致不好定义与其相对应的结构体时,我们可以使用 json.RawMessage
保存原始字节数据。
RawMessage 本质上是 []byte
类型,保存的是原始编码的 JSON 值。它实现了 Marshaler 和 Unmarshaler,可用于延迟 JSON 解码或预计算 JSON 编码。
我们可以把 RawMessage 看作是一部分可以暂时忽略的信息,以后可以进一步去解析,但此时不用。所以,我们保留它的原始形式,还是个字节数组即可。
[]byte
类型的字段,在序列化为 JSON 时会被转换为 base64 编码的字符串:
func main() {
h := []byte(`{"precomputed": true}`)
c := struct {
Header []byte `json:"header"`
Body string `json:"body"`
}{Header: h, Body: "Hello Gophers!"}
b, err := json.Marshal(c)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("c:", string(b))
// c: {"header":"eyJwcmVjb21wdXRlZCI6IHRydWV9","body":"Hello Gophers!"}
raw, err := base64.StdEncoding.DecodeString("eyJwcmVjb21wdXRlZCI6IHRydWV9")
if err != nil {
fmt.Println(err)
return
}
fmt.Println("raw: ", string(raw))
// raw: {"precomputed": true}
}
将 []byte
替换为 json.RawMessage
即可保持原样:
func main() {
h := []byte(`{"precomputed": true}`)
c := struct {
Header json.RawMessage `json:"header"`
Body string `json:"body"`
}{Header: h, Body: "Hello Gophers!"}
b, err := json.Marshal(c)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("c:", string(b))
// c: {"header":{"precomputed":true},"body":"Hello Gophers!"}
}
如何解决 Golang JSON 序列化时 HTML 特殊字符被转义的问题
Go 在序列化 JSON 时,默认会将 &
、<
和>
转义为 \u0026
、\u003c
和 \u003e
,以避免在 HTML 中嵌入 JSON 时可能出现的某些安全问题。
有些业务场景下可能不想被转义,比如需要序列化带查询参数的 URL,这种场景下我们并不希望转义 &
符号,可以通过 SetEscapeHTML(false)
禁用此行为。
type URLInfo struct {
URL string
}
func main() {
data := URLInfo{URL: "https://blog.axiaoxin.com?page=1&size=10"}
b, err := json.Marshal(data)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("b: %s\n", b)
buf := bytes.Buffer{}
encoder := json.NewEncoder(&buf)
encoder.SetEscapeHTML(false) // 不转义 HTML 特殊字符
if err := encoder.Encode(data); err != nil {
fmt.Println(err)
return
}
fmt.Printf("buf: %s\n", buf.String())
}
运行示例,输出结果:
b: {"URL":"https://blog.axiaoxin.com?page=1\u0026size=10"}
buf: {"URL":"https://blog.axiaoxin.com?page=1&size=10"}