Go 学习笔记(四十一)Go 处理 json 数据

本文原创地址:博客园骏马金龙Go 处理 json 数据

Go json 包

Marshal():Go 数据对象 -> json 数据
UnMarshal():Json 数据 -> Go 数据对象

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

构建 json 数据

Marshal()和 MarshalIndent() 函数可以将数据封装成 json 数据。

  • struct、slice、array、map 都可以转换成 json
  • struct 转换成 json 的时候,只有字段首字母大写的才会被转换
  • map 转换的时候,key 必须为 string
  • 封装的时候,如果是指针,会追踪指针指向的对象进行封装

例如:

有一个 struct 结构:

type Post struct {
	Id      int
	Content string
	Author  string
}

这个结构表示博客文章类型,有文章 ID,文章内容,文章的提交作者。这没什么可说的,唯一需要指明的是: 它是一个 struct,struct 可以封装 (编码) 成 JSON 数据

要将这段 struct 数据转换成 json,只需使用 Marshal() 即可。如下:

post := &Post{1, "Hello World", "userA"}
b, err := json.Marshal(post)
if err != nil {
	fmt.Println(nil)
}

Marshal() 返回的是一个[]byte类型,现在变量 b 就已经存储了一段[]byte类型的 json 数据,可以输出它:

fmt.Println(string(b))

结果:

{"Id":1,"Content":"Hello World","Author":"userA"}

可以在封装成 json 的时候进行 "美化",使用 MarshalIndent()即可自动添加前缀 (前缀字符串一般设置为空) 和缩进:

c,err := json.MarshalIndent(post,"","\t")
if err != nil {
	fmt.Println(nil)
}
fmt.Println(string(c))

结果:

{
	"Id": 1,
	"Content": "Hello World",
	"Author": "userA"
}

除了 struct,array、slice、map 结构都能解析成 json,但是 map 解析成 json 的时候,key 必须只能是 string,这是 json 语法要求的。

例如:

// slice -> json
s := []string{"a", "b", "c"}
d, _ := json.MarshalIndent(s, "", "\t")
fmt.Println(string(d))

// map -> json
m := map[string]string{
	"a":"aa",
	"b":"bb",
	"c":"cc",
}
e,_ := json.MarshalIndent(m,"","\t")
fmt.Println(string(e))

返回结果:

[
	"a",
	"b",
	"c"
]
{
	"a": "aa",
	"b": "bb",
	"c": "cc"
}

使用 struct tag 辅助构建 json

struct 能被转换的字段都是首字母大写的字段,但如果想要在 json 中使用小写字母开头的 key,可以使用 struct 的 tag 来辅助反射。

例如,Post 结构增加一个首字母小写的字段 createAt。

type Post struct {
    Id      int      `json:"ID"`
    Content string   `json:"content"`
    Author  string   `json:"author"`
    Label   []string `json:"label"`
}


postp := &Post{
    2,
    "Hello World",
    "userB",
    []string{"linux", "shell"},
    }

p, _ := json.MarshalIndent(postp, "", "\t")
fmt.Println(string(p))

结果:

{
    "ID": 2,
    "content": "Hello World",
    "author": "userB",
    "label": [
        "linux",
        "shell"
    ]
}

使用 struct tag 的时候,几个注意点:

  1. tag 中标识的名称将称为 json 数据中 key 的值
  2. tag 可以设置为json:"-"来表示本字段不转换为 json 数据,即使这个字段名首字母大写
    • 如果想要 json key 的名称为字符 "-",则可以特殊处理json:"-,",也就是加上一个逗号
  3. 如果 tag 中带有,omitempty选项,那么如果这个字段的值为 0 值,即 false、0、""、nil 等,这个字段将不会转换到 json 中
  4. 如果字段的类型为 bool、string、int 类、float 类,而 tag 中又带有,string选项,那么这个字段的值将转换成 json 字符串

例如:

type Post struct {
    Id      int      `json:"ID,string"`
    Content string   `json:"content"`
    Author  string   `json:"author"`
    Label   []string `json:"label,omitempty"`
}

解析 json 数据到 struct(结构已知)

json 数据可以解析到 struct 或空接口interface{}中 (也可以是 slice、map 等)。理解了上面构建 json 时的 tag 规则,理解解析 json 就很简单了。

例如,下面是一段 json 数据:

{
    "id": 1,
    "content": "hello world",
    "author": {
        "id": 2,
        "name": "userA"
    },
    "published": true,
    "label": [],
    "nextPost": null,
    "comments": [{
            "id": 3,
            "content": "good post1",
            "author": "userB"
        },
        {
            "id": 4,
            "content": "good post2",
            "author": "userC"
        }
    ]
}

分析下这段 json 数据:

  1. 顶层的大括号表示是一个匿名对象,映射到 Go 中是一个 struct,假设这个 struct 名称为 Post
  2. 顶层大括号里的都是 Post 结构中的字段,这些字段因为都是 json 数据,所以必须都首字母大写,同时设置 tag,tag 中的名称小写
  3. 其中 author 是一个子对象,映射到 Go 中是另一个 struct,在 Post 中这个字段的名称为 Author,假设名称和 struct 名称相同,也为 Author
  4. label 是一个数组,映射到 Go 中可以是 slice,也可以是 array,且因为 json array 为空,所以 Go 中的 slice/array 类型不定,比如可以是 int,可以是 string,也可以是interface{},对于这里的示例来说,我们知道标签肯定是 string
  5. nextPost 是一个子对象,映射到 Go 中是一个 struct,但因为 json 中这个对象为 null,表示这个对象不存在,所以无法判断映射到 Go 中 struct 的类型。但对此处的示例来说,是没有下一篇文章,所以它的类型应该也是 Post 类型
  6. comment 是子对象,且是数组包围的,映射到 Go 中,是一个 slice/array,slice/array 的类型是一个 struct

分析之后,对应地去构建 struct 和 struct 的 tag 就很容易了。如下,是根据上面分析构建出来的数据结构:

type Post struct {
    ID        int64         `json:"id"`       
    Content   string        `json:"content"`  
    Author    Author        `json:"author"`   
    Published bool          `json:"published"`
    Label     []string      `json:"label"`    
    NextPost  *Post         `json:"nextPost"` 
    Comments  []*Comment    `json:"comments"` 
}

type Author struct {
    ID   int64  `json:"id"`  
    Name string `json:"name"`
}

type Comment struct {
    ID      int64  `json:"id"`     
    Content string `json:"content"`
    Author  string `json:"author"` 
}

注意,前面在介绍构建 json 数据的时候说明过,指针会进行追踪,所以这里反推出来的 struct 中使用指针类型是没问题的。

于是,解析上面的 json 数据到 Post 类型的对象中,假设这个 json 数据存放在 a.json 文件中。代码如下:

func main() {
    // 打开json文件
    fh, err := os.Open("a.json")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer fh.Close()
    // 读取json文件,保存到jsonData中
    jsonData, err := ioutil.ReadAll(fh)
    if err != nil {
        fmt.Println(err)
        return
    }
    
    var post Post
    // 解析json数据到post中
    err = json.Unmarshal(jsonData, &post)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(post)
}

输出结果:

{1 hello world {2 userA} true [] <nil> [0xc042072300 0xc0420723c0]}

也许你已经感受到了,从 json 数据反推算 struct 到底有多复杂,虽然逻辑不难,但如果数据复杂一点,这是件非常恶心的事情。所以,使用别人写好的工具来自动转换吧。本文后面有推荐 json 到数据结构的自动转换工具。

解析 json 到 interface(结构未知)

上面是已知 json 数据结构的解析方式,如果 json 结构是未知的或者结构可能会发生改变的情况,则解析到 struct 是不合理的。这时可以解析到空接口interface{}map[string]interface{}类型上,这两种类型的结果是完全一致的。

解析到interface{}上时,Go 类型和 JSON 类型的对应关系如下

  JSON类型             Go类型                
---------------------------------------------
JSON objects    <-->  map[string]interface{} 
JSON arrays     <-->  []interface{}          
JSON booleans   <-->  bool                   
JSON numbers    <-->  float64                
JSON strings    <-->  string                 
JSON null       <-->  nil                    

例如:

func main() {
    // 读取json文件
    fh, err := os.Open("a.json")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer fh.Close()
    jsonData, err := ioutil.ReadAll(fh)
    if err != nil {
        fmt.Println(err)
        return
    }
    
    // 定义空接口接收解析后的json数据
    var unknown interface{}
    // 或:map[string]interface{} 结果是完全一样的
    err = json.Unmarshal(jsonData, &unknown)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(unknown)
}

输出结果:

map[nextPost:<nil> comments:[map[id:3 content:good post1
author:userB] map[id:4 content:good post2 author:userC]]
id:1 content:hello world author:map[id:2 name:userA] published:true label:[]]

上面将输出 map 结构。这是显然的,因为类型对应关系中已经说明了,json object 解析到 Go interface 的时候,对应的是 map 结构。如果将上面输出的结构进行一下格式化,得到的将是类似下面的结构:

map[
    nextPost:<nil>
    comments:[
        map[
            id:3
            content:good post1
            author:userB
        ]
        map[
            id:4
            content:good post2
            author:userC
        ]
    ]
    id:1
    content:hello world
    author:map[
        id:2
        name:userA
    ]
    published:true
    label:[]
]

现在,可以从这个 map 中去判断类型、取得对应的值。但是如何判断类型?可以使用类型断言:

func main() {
    // 读取json数据
    fh, err := os.Open("a.json")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer fh.Close()
    jsonData, err := ioutil.ReadAll(fh)
    if err != nil {
        fmt.Println(err)
        return
    }
    
    // 解析json数据到interface{}
    var unknown interface{}
    err = json.Unmarshal(jsonData, &unknown)
    if err != nil {
        fmt.Println(err)
        return
    }

    // 进行断言,并switch匹配
    m := unknown.(map[string]interface{})
    for k, v := range m {
        switch vv := v.(type) {
        case string:
            fmt.Println(k, "type: string\nvalue: ", vv)
            fmt.Println("------------------")
        case float64:
            fmt.Println(k, "type: float64\nvalue: ", vv)
            fmt.Println("------------------")
        case bool:
            fmt.Println(k, "type: bool\nvalue: ", vv)
            fmt.Println("------------------")
        case map[string]interface{}:
            fmt.Println(k, "type: map[string]interface{}\nvalue: ", vv)
            for i, j := range vv {
                fmt.Println(i,": ",j)
            }
            fmt.Println("------------------")
        case []interface{}:
            fmt.Println(k, "type: []interface{}\nvalue: ", vv)
            for key, value := range vv {
                fmt.Println(key, ": ", value)
            }
            fmt.Println("------------------")
        default:
            fmt.Println(k, "type: nil\nvalue: ", vv)
            fmt.Println("------------------")
        }
    }
}

结果如下:

comments type: []interface{}
value:  [map[id:3 content:good post1 author:userB] map[author:userC id:4 content:good post2]]
0 :  map[id:3 content:good post1 author:userB]
1 :  map[id:4 content:good post2 author:userC]
------------------
id type: float64
value:  1
------------------
content type: string
value:  hello world
------------------
author type: map[string]interface{}
value:  map[id:2 name:userA]
name :  userA
id :  2
------------------
published type: bool
value:  true
------------------
label type: []interface{}
value:  []
------------------
nextPost type: nil
value:  <nil>
------------------

可见,从 interface 中解析非常复杂,而且可能因为嵌套结构而导致无法正确迭代遍历。这时候,可以使用第三方包 simplejson,见后文。

解析、创建 json 流

除了可以直接解析、创建 json 数据,还可以处理流式数据。

  • type Decoder 解码 json 到 Go 数据结构
  • type Encoder 编码 Go 数据结构到 json

例如:

const jsonStream = `
    {"Name": "Ed", "Text": "Knock knock."}
    {"Name": "Sam", "Text": "Who's there?"}
    {"Name": "Ed", "Text": "Go fmt."}
    {"Name": "Sam", "Text": "Go fmt who?"}
    {"Name": "Ed", "Text": "Go fmt yourself!"}
`
type Message struct {
    Name, Text string
}
dec := json.NewDecoder(strings.NewReader(jsonStream))
for {
    var m Message
    if err := dec.Decode(&m); err == io.EOF {
        break
    } else if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%s: %s\n", m.Name, m.Text)
}

输出:

Ed: Knock knock.
Sam: Who's there?
Ed: Go fmt.
Sam: Go fmt who?
Ed: Go fmt yourself!

再例如,从标准输入读 json 数据,解码后删除名为 Name 的元素,最后重新编码后输出到标准输出。

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)
        }
    }
}

json 转 Go 数据结构工具推荐

quicktype 工具,可以轻松地将 json 文件转换成各种语言对应的数据结构。

地址:https://quicktype.io

在 vscode 中有相关插件

  1. 先在命令面板中输入 "set quicktype target language" 选择要将 json 转换成什么语言的数据结构 (比如 Go)
  2. 再输入 "open quicktype for json" 就可以将当前 json 文件转换对应的数据结构 (比如 struct)

转换后只需按实际的需求稍微修改一部分类型即可。比如为 json 顶级匿名对象对应的 struct 设定名称,还有一些无法转换成 struct 时因为判断数据类型而使用的interface{}类型也要改一改。

例如,下面是使用 quicktype 工具对前面示例 json 数据进行转换后的数据结构:

type A struct {
    ID        int64         `json:"id"`       
    Content   string        `json:"content"`  
    Author    Author        `json:"author"`   
    Published bool          `json:"published"`
    Label     []interface{} `json:"label"`    
    NextPost  interface{}   `json:"nextPost"` 
    Comments  []Comment     `json:"comments"` 
}

type Author struct {
    ID   int64  `json:"id"`  
    Name string `json:"name"`
}

type Comment struct {
    ID      int64  `json:"id"`     
    Content string `json:"content"`
    Author  string `json:"author"` 
}

其中需要将type A struct的 A 改成你自己的名称,将 A 中的interface{}也改成合理的类型。
上一篇 Go 学习笔记(四十)Go template 用法详解
Go 学习笔记(目录)