Golang JSON 完整指南

文章目录

这篇文章记录在日常的 Golang 开发中,经常会使用到的 JSON 处理技巧。

JSON(JavaScript Object Notation)是一种简单的数据交换格式。从语法上讲,它类似于 JavaScript 的对象和列表。它最常用于 Web 后端和在浏览器中运行的 JavaScript 程序之间的通信,但它也用于许多其他地方。它的主页 <json.org> 提供了非常清晰和简洁的标准定义。

使用 json 包,可以轻而易举地从 Go 程序中读写 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}

编码

我们使用 Marshal 函数对 JSON 数据进行编码(序列化):

func Marshal(v interface{}) ([]byte, error)

只有可以表示为有效 JSON 的数据结构才会被编码:

  • JSON 对象只支持字符串作为键;要对 Go 的 map 类型进行编码,它必须采用 map[string]T 形式(其中 T 是 json 包支持的任何 Go 类型)。
  • 无法对 channelcomplexfunction 类型进行编码。
  • 不支持循环数据结构;它们会使 Marshal 进入无限循环。
  • 指针将被编码为它们指向的值(如果指针为 nil,则为 null)。

json 包只访问结构体类型的导出字段(以大写字母开头的字段)。因此,只有结构体的导出字段才会出现在 JSON 输出中

要解码(反序列化) JSON 数据,我们使用 Unmarshal 函数:

func Unmarshal(data []byte, v interface{}) error

解码

Unmarshal 如何识别存储解码数据的字段?对于一个给定的 JSON 键 FooUnmarshal 将遍历目标结构体的字段,以找到(按以下优先级顺序):

  1. 带有 Foo 标签的导出字段,
  2. 名为 Foo 的导出字段,或
  3. 名为 FOOFoO 的导出字段或 Foo 的其他不区分大小写匹配的导出字段。

当 JSON 数据的结构与 Go 数据类型不完全匹配时会发生什么?

b := []byte(`{"Name":"Bob","Food":"Pickle"}`)
var m Message
err := json.Unmarshal(b, &m)

Unmarshal 将只解码它可以在目标类型中找到的字段。在这种情况下,只会填充 mName 字段,而忽略 Food 字段。当您希望从大型 JSON blob 中仅选择几个特定字段时,此行为特别有用。这也意味着目标结构体中任何未导出的字段都不会受到 Unmarshal 的影响。

但是,如果您事先不知道 JSON 数据的结构怎么办?

带接口的通用 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{} 值。

类型关系

JSON 和 Go 类型不是一对一匹配的。下表描述了编码和解码时的类型关系。

Go 类型 JSON 类型
bool boolean
float64 number
string string
nil null
time.Time RFC 3339 时间字符串
数组或切片(除 []byte 外) 数组

您会注意到缺少 float32int 类型。别担心,您当然可以将数字编码和解码为这些类型,它们只是在 JSON 规范中没有明确的类型。例如,如果你在 JSON 中编码一个整数,它保证没有小数点。但是,如果有人在您解码之前将该 JSON 值转换为浮点数,您将收到运行时错误。

序列化 JSON 数据时很少会遇到错误,但反序列化 JSON 经常会导致错误。以下是一些需要注意的事项:

  • 任何类型冲突都会导致错误。例如,您不能将字符串解码为 int,即使字符串化数字:"speed": "42"
  • 浮点数无法解码为整数
  • null 不能被解码为没有 nil 选项的值。例如,如果您有一个可以为空的数字字段,您应该解码为 *int
  • time.Time 默认只能解码 RFC 3339 字符串 - 其他类型的时间将失败
  • 数组和切片值编码为 JSON 数组,除了 []byte 编码为 base64 编码的字符串

解码任意数据

下面这个存储在变量 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 数据,同时仍然享受类型安全的好处。

运行完整示例

引用类型

让我们定义一个 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 指针。

流编码器和解码器

json 包提供了 DecoderEncoder 类型来支持读写 JSON 数据流的通用操作。 NewDecoderNewEncoder 函数包装了 io.Readerio.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 无处不在,这些 EncoderDecoder 类型可用于广泛的场景,例如读取和写入 HTTP 连接、WebSocket 或文件。

读写 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
}

结构体 json 标签

Go struct 标签是出现在 Go struct 声明中类型之后的由反引号(`)字符包裹的注解。json 标签主要在 struct 与 JSON 数据转换的过程中使用。

使用 json 标签指定键名

type User struct {
    FirstName string `json:"first_name"` // JSON 键为 "first_name"
    BirthYear int `json:"birth_year"` // JSON 键为 "birth_year"
    Email string // JSON 键为 "Email"
}

忽略空值字段:omitempty

当 struct 中的字段没有值时, json.Marshal() 时候默认输出没有值的字段的类型零值(例如 intfloat 类型零值是 0string 类型零值是 "",对象类型零值是 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}
}

忽略指定字段:-

如上所述,json.Marshal 会忽略未导出(小写)字段。如果想忽略其他字段,可以使用 - 标签。

标签选项添加 - 可以忽略该字段的序列化:

type User struct {
  FirstName string `json:"first_name"`
  BirthYear int `json:"birth_year"`
  Password string `json:"-"`
}

// json.Marshal 结果中将没有 Password 字段

字符串传递数字: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 类型则会报错。

整数变科学计数法浮点数

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 类型,它实现了 Int64Float64 方法,使用者可以按需转换。

使用 MarshalJSON 支持 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}

使用匿名结构体处理 JSON

使用匿名结构体添加或删除 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"}

使用匿名结构体进行组合或拆分 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}

使用 json.RawMessage 处理 []byte 变为 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!"}
}

运行示例

序列化时不转义 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"}

相关阅读:Golang JSON 序列化时 HTML 特殊字符转义问题分析


也可以看看


全国大流量卡免费领

19元月租ㆍ超值优惠ㆍ长期套餐ㆍ免费包邮ㆍ官方正品