31.Go JSON

Go中的标准库encoding/json提供JSON格式的序列化和反序列化功能.

序列化struct为JSON

type Person struct {
    fullName string
    Name     string
    Age      int    `json:"age"`
    City     string `json:"city"`
}

p := Person{
    Name: "John",
    Age:  37,
    City: "SF",
}
d, err := json.Marshal(&p)
if err != nil {
    log.Fatalf("json.MarshalIndent failed with '%s'\n", err)
}
fmt.Printf("Person in compact JSON: %s\n", string(d))

d, err = json.MarshalIndent(p, "", "  ")
if err != nil {
    log.Fatalf("json.MarshalIndent failed with '%s'\n", err)
}
fmt.Printf("Person in pretty-printed JSON:\n%s\n", string(d))

Person in compact JSON: {"Name":"John","age":37,"city":"SF"}
Person in pretty-printed JSON:
{
"Name": "John",
"age": 37,
"city": "SF"
}

json.Marshal和json.MarshalIndent都将interface {}作为第一个参数。我们可以传递任何Go值,并将其类型包装到interface {}中。

Marshaller将使用反射来检查传递的值并将其编码为JSON字符串。

在序列化struct时,仅对导出的字段(其名称以大写字母开头)进行序列化/反序列化。

在我们的示例中,未对fullName进行序列化。

struct被序列化为JSON字典。默认情况下,字典键与struct字段名称相同。

struct字段名称在字典键名称下序列化。

可以提供带有struct标签的自定义映射。

可以将任意的struct标签字符串附加到struct字段。

json:"age"指示JSON编码器/解码器使用名称age作为表示字段Age的字典关键字。

序列化struct时,将值和指针传递给它会产生相同的结果。

传递指针效率更高,因为按值传递会创建不必要的副本。

json.MarshallIndent格式化打印嵌套struct, 这样会占用更多空间但更易于阅读。

把JSON转为struct

type Person struct {
    Name       *string `json:"name"`
    Age        int     `json:"age"`
    City       string
    Occupation string
}

var jsonStr = `{
    "name": "Jane",
    "age": 24,
    "city": "ny"
}`

var p Person
err := json.Unmarshal([]byte(jsonStr), &p)
if err != nil {
    log.Fatalf("json.Unmarshal failed with '%s'\n", err)
}
fmt.Printf("Person struct parsed from JSON: %#v\n", p)
fmt.Printf("Name: %#v\n", *p.Name)

Person struct parsed from JSON: main.Person{Name:(*string)(0xc000010330), Age:24, City:"ny", Occupation:""}
Name: "Jane"

解析与序列化相反

与序列化不同,在解析为结构时,必须将指针传递给struct。否则json.Unmarshal将接收并修改该结构的副本,而不是结构本身。从json.Unmarshal返回后,该副本将被丢弃。

请注意,即使名称不匹配,JSON元素city也被解码为City struct字段,并且我们没有使用json struct标签提供显式映射。

发生这种情况是因为在将字典关键字名称与结构字段名称进行匹配时,JSON解码器具有一些技巧。最好不要依赖这种智能,而是明确定义映射。

所有struct字段都是可选的,并且当不以JSON文本形式出现时,其值将保持不变。当解码为新初始化的struct时,对于给定类型,其值为零。

字段名称显示JSON解码器还可以自动将其解码为指向值的指针。

当您需要知道JSON中是否存在值时,这很有用。如果我们使用字符串作为Name,我们将不知道空字符串的值是否意味着JSON具有以空字符串作为值的名称键,或者是因为该值根本不存在。

通过使用指向字符串的指针,我们知道nil表示没有值。

JSON和Go类型映射:

  • JSON类型 Go类型
  • boolean bool
  • number float64 or int
  • string string
  • array slice
  • dictionary map[struct]interface{} or struct
  • null nil

解析任意JSON

解析为一个结构非常方便,但有时我们不知道JSON的结构。

对于任意JSON,我们可以解码为map[string]interface{},它可以表示任意有效的JSON。

var jsonStr = `{
    "name": "Jane",
    "age": 24,
    "city": "ny"
}`

var doc map[string]interface{}
err := json.Unmarshal([]byte(jsonStr), &doc)
if err != nil {
    log.Fatalf("json.Unmarshal failed with '%s'\n", err)
}
fmt.Printf("doc: %#v\n", doc)
name, ok := doc["name"].(string)
if !ok {
    log.Fatalf("doc has no key 'name' or its value is not string\n")
}
fmt.Printf("name: %#v\n", name)

doc: map[string]interface {}{"age":24, "city":"ny", "name":"Jane"}
name: "Jane"

对于基本JSON类型,映射中的值为bool,int,float64或string。

对于JSON数组,该值为[] interface {}。

对于JSON字典,该值是(再次)map [string] interface {}。

这种方法很灵活,但是处理map [string] interface {}以访问值是很痛苦的。

反序列化为匿名struct

解析为结构时,我们可以使用匿名struct来避免声明结构类型。

var jsonBlob = []byte(`
{
  "_total": 1,
  "_links": {
    "self": "https://api.twitch.tv/kraken/channels/foo/subscriptions?direction=ASC&limit=25&offset=0",
    "next": "https://api.twitch.tv/kraken/channels/foo/subscriptions?direction=ASC&limit=25&offset=25"
  },
  "subscriptions": [
    {
      "created_at": "2011-11-23T02:53:17Z",
      "_id": "abcdef0000000000000000000000000000000000",
      "_links": {
        "self": "https://api.twitch.tv/kraken/channels/foo/subscriptions/bar"
      },
      "user": {
        "display_name": "bar",
        "_id": 123456,
        "name": "bar",
        "created_at": "2011-06-16T18:23:11Z",
        "updated_at": "2014-10-23T02:20:51Z",
        "_links": {
          "self": "https://api.twitch.tv/kraken/users/bar"
        }
      }
    }
  ]
}
`)

var js struct {
    Total int `json:"_total"`
    Links struct {
        Next string `json:"next"`
    } `json:"_links"`
    Subs []struct {
        Created string `json:"created_at"`
        User    struct {
            Name string `json:"name"`
            ID   int    `json:"_id"`
        } `json:"user"`
    } `json:"subscriptions"`
}

err := json.Unmarshal(jsonBlob, &js)
if err != nil {
    fmt.Println("error:", err)
}
fmt.Printf("%+v", js)

{Total:1 Links:{Next:https://api.twitch.tv/kraken/channels/foo/subscriptions?direction=ASC&limit=25&offset=25} Subs:[{Created:2011-11-23T02:53:17Z User:{Name:bar ID:123456}}]}

从文件反序列化JSON

我们可以从磁盘上的文件,或者任何io.Reader,比如网络连接来反序列化JSON.
下面的例子从文件读取JSON并反序列化:

type Student struct {
    Name     string
    Standard int `json:"Standard"`
}

func decodeFromReader(r io.Reader) ([]*Student, error) {
    var res []*Student

    dec := json.NewDecoder(r)
    err := dec.Decode(&res)
    if err != nil {
        return nil, err
    }
    return res, nil
}

func decodeFromString(s string) ([]*Student, error) {
    r := bytes.NewBufferString(s)
    return decodeFromReader(r)
}

func decodeFromFile(path string) ([]*Student, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer f.Close()
    return decodeFromReader(f)
}

Student: John Doe, standard: 4
Student: Peter Parker, standard: 11
Student: Bilbo Baggins, standard: 150

通过编写帮助函数decodeFromReader,我们可以轻松编写可用于文件,字符串或网络连接的包装器。

配置JSON序列化

隐藏/跳过某些字段

要导出Revenue和sales但不对它们进行编码/解码,请使用json:“-”或重命名变量以小写字母开头。 请注意,这将防止变量在包外部可见。

type Company struct {
    Name     string `json:"name"`
    Location string `json:"location"`
    Revenue  int    `json:"-"`
    sales    int
}

忽略空字段

为了防止Location设置为零值时将其包含在JSON中,请将,omitempty添加到json标记中。

type Company struct {
    Name     string `json:"name"`
    Location string `json:"location,omitempty"`
}

自定义JSON序列化

编写自定义JSON序列化
有时,类型没有明显的JSON映射。

如何序列化time.Time? 有很多可能性。

Go为time.Time提供了默认的JSON映射。 我们可以为用户定义的类型(例如struct)实现自定义序列化。

对于现有类型,我们可以定义一个新的(但兼容)类型。

这是时间的自定义序列化。时间仅序列化年/月/日部分:

type Event struct {
    What string
    When time.Time
}
e := Event{
    What: "earthquake",
    When: time.Now(),
}
d, err := json.Marshal(&e)
if err != nil {
    log.Fatalf("json.MarshalIndent failed with '%s'\n", err)
}
fmt.Printf("Standard time JSON: %s\n", string(d))

type customTime time.Time

const customTimeFormat = `"2006-02-01"`

func (ct customTime) MarshalJSON() ([]byte, error) {
    t := time.Time(ct)
    s := t.Format(customTimeFormat)
    return []byte(s), nil
}

func (ct *customTime) UnmarshalJSON(d []byte) error {
    t, err := time.Parse(customTimeFormat, string(d))
    if err != nil {
        return err
    }
    *ct = customTime(t)
    return nil
}

type Event2 struct {
    What string
    When customTime
}

e := Event2{
    What: "earthquake",
    When: customTime(time.Now()),
}
d, err := json.Marshal(&e)
if err != nil {
    log.Fatalf("json.Marshal failed with '%s'\n", err)
}
fmt.Printf("\nCustom time JSON: %s\n", string(d))
var decoded Event2
err = json.Unmarshal(d, &decoded)
if err != nil {
    log.Fatalf("json.Unmarshal failed with '%s'\n", err)
}
t := time.Time(decoded.When)
fmt.Printf("Decoded custom time: %s\n", t.Format(customTimeFormat))

notCustom()
custom()

Standard time JSON: {"What":"earthquake","When":"2019-11-06T02:18:11.203193337Z"}
Custom time JSON: {"What":"earthquake","When":"2019-06-11"}
Decoded custom time: "2019-06-11"

请注意,UnmashalJSON的接收者类型是指向该类型的指针。

这对于将更改保留在函数本身之外是必要的。

带私有字段的封送结构
考虑具有已导出和未导出字段的结构:

type MyStruct struct {
    uuid string
    Name string
}

想象一下,您想将该结构Marshal()转换为有效的JSON,以便存储在etcd之类的文件中。

但是,由于未导入uuid,因此json.Marshal()跳过了它。

要封送私有字段而不将其公开,我们可以使用自定义封送程序:

type MyStruct struct {
    uuid string
    Name string
}

func (m MyStruct) MarshalJSON() ([]byte, error) {
    j, err := json.Marshal(struct {
        Uuid string
        Name string
    }{
        Uuid: m.uuid,
        Name: m.Name,
    })
    if err != nil {
        return nil, err
    }
    return j, nil
}

s := MyStruct{
    uuid: "uid-john",
    Name: "John",
}
d, err := json.Marshal(&s)
if err != nil {
    log.Fatalf("json.MarshalIndent failed with '%s'\n", err)
}
fmt.Printf("Person in compact JSON: %s\n", string(d))

Person in compact JSON: {"Uuid":"uid-john","Name":"John"}

幕后的自定义封送处理
自定义封送处理如何工作?

包JSON定义了2个接口:Marshaler和Unmarshaler。

type Marshaler interface {
    MarshalJSON() ([]byte, error)
}

type Unmarshaler interface {
    UnmarshalJSON([]byte) error
}

通过实现这些功能,我们使我们的类型符合Marshaler或Unmarshaler接口。

JSON编码器/解码器检查被编码的值是否符合那些接口,并将调用这些函数而不执行默认逻辑。

Go和JSON类型映射

JSON type Go type
null nil
boolean bool
number float64 or int
string string
array slice
dictionary map[struct]interface{} or struct

使用示例:

func printSerialized(v interface{}, w io.Writer) {
    d, err := json.Marshal(v)
    if err != nil {
        log.Fatalf("json.Marshal failed with '%s'\n", err)
    }
    fmt.Fprintf(w, "%T\t%s\n", v, string(d))
}

w := new(tabwriter.Writer)
w.Init(os.Stdout, 5, 0, 1, ' ', 0)
fmt.Fprint(w, "Go type:\tJSON value:\n")
fmt.Fprint(w, "\t\n")
printSerialized(nil, w)
printSerialized(5, w)
printSerialized(8.23, w)
printSerialized("john", w)
ai := []int{5, 4, 18}
printSerialized(ai, w)
a := []interface{}{4, "string"}
printSerialized(a, w)
d := map[string]interface{}{
    "i": 5,
    "s": "foo",
}
printSerialized(d, w)
s := struct {
    Name string
    Age  int
}{
    Name: "John",
    Age:  37,
}
printSerialized(s, w)
w.Flush()

Go type: JSON value:

<nil> null
int 5
float64 8.23
string "john"
[]int [5,4,18]
[]interface {} [4,"string"]
map[string]interface {} {"i":5,"s":"foo"}
struct { Name string; Age int } {"Name":"John","Age":37}

轻松生成JSON结构定义

编写映射JSON文件结构的结构定义很繁琐。

如果您有示例JSON文件,则可以使用在线工具自动生成Go定义:

https://app.quicktype.io/
https://mholt.github.io/json-to-go/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 210,914评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 89,935评论 2 383
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,531评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,309评论 1 282
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,381评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,730评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,882评论 3 404
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,643评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,095评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,448评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,566评论 1 339
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,253评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,829评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,715评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,945评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,248评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,440评论 2 348

推荐阅读更多精彩内容

  • 前言 本文主要根据Go语言Json包[1]、官方提供的Json and Go[2]和go-and-json[3]整...
    TOMOCAT阅读 162评论 0 0
  • 本文是基于 Go 官方和 https://eager.io/blog/go-and-json/ 进行翻译整理的 J...
    小蜗牛爬楼梯阅读 491评论 0 0
  • JSON(JavaScript Object Notation,JS对象标记)是一种比XML更为轻量的【数据交换格...
    JunChow520阅读 888评论 0 1
  • 参考:https://sanyuesha.com/2018/05/07/go-json/[https://sany...
    天空蓝雨阅读 4,215评论 0 3
  • fmt格式化字符串 格式:%[旗标][宽度][.精度][arg索引]动词旗标有以下几种:+: 对于数值类型总是输出...
    皮皮v阅读 1,090评论 0 3