JSON
JavaScript对象表示法(JSON)是一种数据格式,自2000年代初推出以来便广受欢迎。它已成为跨系统(如API请求体)数据传输中无处不在的标准,逐渐取代了XML等早期格式。
Go语言凭借其强类型特性以及对简洁与高效的高度重视,在处理JSON数据方面展现出了极佳的适配性。无论是构建Web应用程序、与API交互,还是将数据存储至数据库中,Go均提供了一系列工具与技术(如标准库中的encoding/json包),使开发者能够高效完成JSON数据的序列化与反序列化操作,同时兼顾代码可维护性和运行时性能。
术语
Go 中的 JSON 有两种关键术语:
- Marshalling(编组): 将 Go 的数据结构转换为有效的 JSON。
- Unmarshalling(解组): 将有效的 JSON 字符串解析为 Go 的数据结构。
在其他编程语言中,Marshalling 通常被称为 Serializing(序列化),而 Unmarshalling 则被称为 Deserializing(反序列化)。
Unmarshalling
import "encoding/json"
func Unmarshal(data []byte, v any) error
Unmarshal 接受两个参数:
- 第一个参数是
[]byte,即要解组的 JSON 对象。 - 第二个参数是
any,期望是目标数据结构的指针,用于存储解组 JSON 数据后的结果。
map
可以将解析后的结果存储到 map 中,这种情况下,JSON 的键值与 map 的键值一一对应。
package main
import (
"encoding/json"
"fmt"
"log"
)
func main() {
input := `{
"name": "John Doe",
"age": 15,
"hobbies": ["climbing", "cycling", "running"]
}`
var target map[string]any
err := json.Unmarshal([]byte(input), &target)
if err != nil {
log.Fatalf("Unable to marshal JSON due to %s", err)
}
// map[age:15 hobbies:[climbing cycling running] name:John Doe]
fmt.Println(target)
}
如果 JSON 字符串是无效的,Unmarshal 将返回一个错误:
package main
import (
"encoding/json"
"fmt"
"log"
)
func main() {
input := `{
"name": "John Doe",
"age": 15,
"hobbies": ["climbing", "cycling", "running"],
}`
if err != nil {
// Unable to marshal JSON due to invalid character '}' looking for beginning of object key string
log.Fatalf("Unable to marshal JSON due to %s", err)
}
}
虽然 map[string]any 可以用于解组 JSON,但它并不是最优的解决方案,主要有以下几个原因:
- 失去类型安全性和编译时检查。Go 语言的静态类型系统提供了类型安全性和编译时检查,而使用
map[string]any会丢失这些优势。这可能会使错误更难被发现,代码的维护变得更加困难。 - 性能较低。与使用结构体或实现
json.Unmarshaler接口的自定义类型相比,map[string]any的性能较低,因为访问映射中的字段需要动态查找,而结构体的字段访问则是在编译时静态确定的,因此更高效。 - 降低代码可读性和可维护性。由于
map[string]any中的值可以是任意类型,这使得理解 JSON 数据的结构变得更加困难,代码可能会变得冗长且容易出错。
struct
当使用结构体 struct 来解组 JSON 对象时,JSON 对象中的字段名会被映射到结构体中的字段名,并相应地赋值。
package main
import (
"fmt"
"log"
)
type Dog struct {
Breed string
Name string
FavoriteTreat string
Age int
}
func main() {
input := `{
"Breed":"Golden Retriever",
"Age":8,
"Name":"Paws",
"FavoriteTreat":"Kibble"
}`
var dog Dog
err := json.Unmarshal([]byte(input), &dog)
if err != nil {
log.Fatalf("Unable to marshal JSON duo to %s", err)
}
// {Golden Retriever Paws Kibble 8}
fmt.Println(dog)
}
- 如果 JSON 包含不属于目标结构体的其他字段,则在解析时会被忽略。
- 如果 JSON 不包含结构体中相应的字段,将导致使用相应字段的零值。
- 解析时不区分大小写,只要字符及顺序相同即可。
- 在定义用于解组 JSON 的结构体时,必须确保结构体字段的名称与 JSON 数据中的键完全匹配。如果名称不匹配,则该字段将不会被填充相应的 JSON 值。
- 如果结构体中包含类型别名字段,那么在解组时,它们的值和类型别名都会被保留。
- 对于嵌套 JSON 对象和数组,如果字段名一一对应,也可正常解析。
Marshalling
json.Marshal() 将一个数据结构转换为 JSON。
基本类型
当使用 Go 中的基本类型时,它产生相应的 JSON。
package main
import (
"encoding/json"
"fmt"
"log"
)
func main() {
first := marshal(14)
second := marshal("Hello world")
third := marshal([]float32{1.66, 6.86, 10.1})
fourth := marshal(map[string]int{"num": 15, "other": 17})
fmt.Printf(
"first: %s\nsecond: %s\nthird: %s\nfourth: %s\n",
first,
second,
third,
fourth,
)
}
func marshal(in any) []byte {
out, err := json.Marshal(in)
if err != nil {
log.Fatalf("Unable to marshal due to %s\n", err)
}
return out
}
struct
在大多数情况下,会遇到更复杂的类型,例如结构体。对这些类型进行编组时,需要更加谨慎,因为 JSON 输出的结构将取决于被编组的 Go 类型的结构。
type Person struct {
Name string
Age uint8
Email string
Phone string
Hobbies []string
}
func main() {
p := Person{
Name: "John Jones",
Age: 26,
Email: "123@xxx.com",
Phone: "132322342",
Hobbies: []string{
"Swimming",
"Badminton",
},
}
b, err := json.Marshal(p)
if err != nil {
log.Fatalf("Unable to marshal due to %s\n", err)
}
// {"Name":"John Jones","Age":26,"Email":"johnjones@email.com","Phone":"89910119","Hobbies":["Swimming","Badminton"]}
fmt.Println(string(b))
}
默认情况下,生成的 JSON 是单行格式,缺少适当的缩进和格式化。虽然这种格式在通过网络传输信息时是理想的,但对于用户而言,它并不是一种友好的 JSON 表示方式。
如果希望格式化 JSON 对象,可以使用 json.MarshalIndent() 方法。它的功能与 json.Marshal() 相同,但会对输出应用缩进,使其更具可读性。
b, err := json.MarshalIndent(p, "", " ")
/*
{
"Name": "John Jones",
"Age": 26,
"Email": "johnjones@email.com",
"Phone": "89910119",
"Hobbies": [
"Swimming",
"Badminton"
]
}
*/
struct tags
在 Go 语言中,结构体标签(struct tags)是一种注解,可以添加到结构体字段上,以提供额外的信息,指导各种工具和库如何处理这些字段。结构体标签是一个字符串,位于字段声明的末尾,并用反引号括起来。
结构体标签最常见的用途是指定结构体在 JSON 编组和解组时的处理方式。通过为结构体字段添加标签,可以控制字段的命名方式、哪些字段应被忽略,以及它们的编码和解码方式。
func main() {
var dog = Dog{
Breed: "Golden Retriever",
Age: 8,
Name: "Paws",
FavoriteTreat: "Kibble",
}
b, err := json.Marshal(dog)
if err != nil {
log.Fatalf("Unable to marshal due to %s\n", err)
}
// {"Breed":"Golden Retriever","Name":"Paws","FavoriteTreat":"Kibble","Age":8}
fmt.Println(string(b))
}
如上,默认情况下,转成 JSON 后,属性和结构体字段名完全相同。
此时,可通过 json 结构体标签来修饰字段:
type Dog struct {
Breed string `json:"breed"`
Name string `json:"name"`
FavoriteTreat string `json:"favorite_treat"`
Age int `json:"age"`
}
// {"breed":"Golden Retriever","name":"Paws","favorite_treat":"Kibble","age":8}
除了自定义字段名称外,结构体标签还可以用于在编组和解组时省略空字段或完全忽略字段。
- 要省略空字段(即在 Go 中具有零值的字段),可添加
omitempty选项。 - 要忽略字段,无论它是否为空,可以使用
json:"-"。
type Person struct {
Name string `json:"name"`
Password string `json:"-"`
Age uint8 `json:"age"`
Email string `json:"email"`
Phone string `json:"phone,omitempty"`
Hobbies []string `json:"hobbies"`
}
Validating
JSON 验证主要有两种形式:
- 验证给定的 JSON 字符串是否正确。
- 检查输入的 JSON 是否符合预定义的模式。
基本验证
可以使用 json.Valid() 方法检查 JSON 的有效性:
func main() {
good := `{"name": "John Doe"}`
bad := `{name: "John Doe"}`
// true
fmt.Println(json.Valid([]byte(good)))
// false
fmt.Println(json.Valid([]byte(bad)))
}
validator
很多情况下,需要对 JSON 进行更复杂的验证,此时可通过使用第三方验证库(如go-playground/validator)实现,其也是依赖结构体标签。
go get github.com/go-playground/validator/v10
要在结构体上实现数据验证,需要使用的验证标签(validate tags),验证规则以 , 分隔,若有属性值则以 = 连接。
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/go-playground/validator/v10"
)
type User struct {
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"required,min=18,max=99"`
}
func main() {
input := `{
"username": "johndoe",
"email": "johndoe@emai",
"age": -14
}`
var user User
err := json.Unmarshal([]byte(input), &user)
if err != nil {
log.Fatalf("Unable to marshal JSON due to %s", err)
}
fmt.Printf("User before validation: %v\n", user)
err = validator.New().Struct(user)
if err != nil {
log.Fatalf("Validation failed due to %v\n", err)
}
}
/*
User before validation: {johndoe johndoe@emai -14}
Validation failed due to Key: 'ValidatedUser.Password' Error:Field validation for 'Password' failed on the 'required' tag
Key: 'ValidatedUser.Email' Error:Field validation for 'Email' failed on the 'email' tag
Key: 'ValidatedUser.Age' Error:Field validation for 'Age' failed on the 'min' tag
*/
在使用 validator 包之前,JSON 已成功解组,结构体标签中的验证规则仅在显式调用时才会生效。
自定义行为
自定义行为在解析各种 JSON 数据时提供了极大的灵活性。
Marshaler
在 Go 中,可以通过实现 json.Marshaler 接口来自定义数据的编组行为。要实现 json.Marshaler 接口,需要定义一个新类型来包装要进行编组的原始类型,该新类型应实现一个名为 MarshalJSON() 的方法,并返回一个字节切片 []byte 和一个错误 error。
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
type CustomTime struct {
time.Time
}
func (ct CustomTime) MarshalJSON() ([]byte, error) {
date := fmt.Sprintf("%q", ct.Time.Format("2006-01-02"))
return []byte(date), nil
}
Unmarshaler
相应地,在解析时可以通过实现 json.Unmarshaler 接口来自定义解析行为。
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
Encoding
Decoder
encoding/json 包还提供了另外两个用于在 Go 中处理 JSON 的方式:json.Encoder 和 json.Decoder。它们本质上与 json.Marshal 和 json.Unmarshal 做的事情相同,但它们操作的是数据流,而不是已经完全加载到内存中的 JSON 对象。
/*
package.json:
{
"name": "react",
"type": "module"
}
*/
type Package struct {
Name string `json:"name"`
Type string `json:"type"`
}
func main() {
pkgFile, err := os.Open("./package.json")
pkg := new(Package)
decoder := json.NewDecoder(pkgFile)
err = decoder.Decode(pkg)
fmt.Println(pkg.Name)
fmt.Println(pkg.Type)
}
json.Decode() 和 json.Unmarshal 之间的一个区别是:前者允许在输入的 JSON 包含与目标中任何未被忽略的导出字段不匹配的属性时显示错误,而后者则简单地忽略这些字段,通过 DisallowUnknownFields() 指定。
func main() {
decoder.DisallowUnknownFields()
}
假设输入的 JSON 如下:
{
"name": "react",
"type": "module",
"main": "index.js"
}
此时,会产生以下错误:
2025/02/08 23:31:42 Unable to decode due to json: unknown field "main"
Encoder
json.Encoder 类型将 Go 类型的 JSON 编码写入提供的可写流 io.Writer。
type TSConfig struct {
Module string `json:"module"`
Target string `json:"target"`
}
func main() {
file, _ := os.Create("./tsconfig.json")
defer file.Close()
var writer io.Writer = file
cfg := TSConfig{
Module: "esnext",
Target: "esnext",
}
encoder := json.NewEncoder(writer)
err := encoder.Encode(cfg)
}
三方包
虽然 encoding/json 相对动态且强大,但它并不是最快的 JSON 包。在对性能有要求的情况下,可考虑使用第三方包: