Golang JSON 處理

· ☕ 4 分鐘

JSON(JavaScript Object Notation)是一種輕量級的資料交換語言,以純文字為基礎去儲存資料,有相容性高、易於理解且許多程式語言都支援等優點。

而在 Golang 之中是如何處理 JSON 的呢?

Go JSON

Golang 官方的 package 中已有支援 JSON,可很容易的對 JSON 數據做編解碼

數據結構

types JSON Golang
字串 string string
整數 number int
浮點數 number flaot64
陣列 array slice
物件 object struct
布林 bool bool
空值 null nil

JSON 編碼

JSON 套件裡面透過 Marshal 函數來將數據處理成 JSON 字串

1
func Marshal(v interface{}) ([]byte, error)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    ID int
    Name string
    Money float64
    Skills []string
    Relationship map[string]string
    Identification Identification
}

type Identification struct {
    Phone bool
    Email bool
}

func main() {
    user := User {
        ID:     1,
        Name:   "Tony",
        Skills: []string{"program", "rich", "play"},
        Relationship: map[string]string {
            "Dad": "Hulk",
            "Mon": "Natasha",
        },
        Identification: Identification {
            Phone: true,
            Email: false,
        },
    }
    b, err := json.Marshal(user)
    if err != nil {
        fmt.Println("error:", err)
    }
    fmt.Println(string(b))
}

輸出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
{   
    "ID":1,
    "Name":"Tony",
    "Money":0,
    "Skills":
    [
        "program",
        "rich","play"
    ],
    "Relationship":
    {
        "Dad":"Hulk",
        "Mon":"Natasha"
    },
    "Identification":
    {
        "Phone":true,
        "Email":false
    }
}

從上面的例子看到 key 值都是大寫,因只有大寫的才會被輸出,但若是需要小寫或其他的名稱怎麼辦呢? Golang 提供了 struct tag 來實現

1
2
3
4
5
6
...
type Identification struct {
    Phone bool `json:"phone"`
    Email bool `json:"email"`
}
...
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
{
    "ID":1,
    "Name":"Tony",
    "Money":0,
    "Skills":
    [
        "program","rich","play"
    ],
    "Relationship":
    {
        "Dad":"Hulk",
        "Mon":"Natasha"
    },
    "Identification":
    {
        "phone":true,
        "email":false
    }
}

struct tag 還可以有以下功能(取自官方文件)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Field appears in JSON as key "myName".
Field int `json:"myName"`

// Field appears in JSON as key "myName" and
// the field is omitted from the object if its value is empty,
// as defined above.
Field int `json:"myName,omitempty"`

// Field appears in JSON as key "Field" (the default), but
// the field is skipped if empty.
// Note the leading comma.
Field int `json:",omitempty"`

// Field is ignored by this package.
Field int `json:"-"`

// Field appears in JSON as key "-".
Field int `json:"-,"`

又或者需要將輸出全部改為 String,Golang 也有提供此功能

1
Int64String int64 `json:",string"`

JSON 解碼

相較 JSON 編碼來說,Golang 對於未知的 JSON 實在是一大挑戰,一開始我們先從已知的結構來看

透過 Unmarshal 將 JSON 字串處理成對應的結構

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

直接來看例子吧

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    ID int
    Name string
    Money float64
    Skills []string
    Relationship map[string]string
    Identification Identification
}

type Identification struct {
    Phone bool `json:"phone"`
    Email bool `json:"email"`
}

func main() {
    var jsonBlob = []byte(`{"ID":1,"Name":"Tony","Money":0,"Skills":["program","rich","play"],"Relationship":{"Dad":"Hulk","Mon":"Natasha"},"Identification":{"phone":true,"email":false}}`)
    
    var user User
    err := json.Unmarshal(jsonBlob, &user)
    if err != nil {
        fmt.Println("error:", err)
    }
    fmt.Printf("%+v", user)
}

解碼與編碼類似,但接受大小寫不同,不過如果欄位是私有的則不會配對

struct tag

如同在編碼的時候,解碼也有對應的 tag,就讓我們來看看吧

  • string tag 表示只有字串會被解析,當傳入的值不是字串時會報錯
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
...
type User struct {
    ID int
    Name string
    Money float64 `json:",string"`
    Skills []string
    Relationship map[string]string
    Identification Identification
}
...

正確:

"Money":"100.5"

錯誤:

"Money":100.5
error: json: invalid use of ,string struct tag, trying to unmarshal unquoted value into float64
  • - 表示不會被解析,但會給予初始值

動態解析 JSON

以上介紹的都是理想的狀況,但實際開發時會常常遇到未知的 JSON 格式,或是動態變化的結構,這時候就可以利用以下方法

interface & assertion

先利用 interface 來儲存任意的數據結構,再利用 assertion 的方式解析數據

1
map[string]interface{}

範例 a:interface 儲存輸入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    var user map[string]interface{}
    var jsonString string = `{"ID":1, "Name":"Tony"}`
    if err := json.Unmarshal([]byte(jsonString), &user); err != nil {
        fmt.Println("ERROR:",err)
    }
    fmt.Printf("%#v\n", user)
}

範例 b:用 assertion 來解析數據

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    ID int
    Name string
    Money float64 `json:",string"`
    Skills []string
    Relationship map[string]string
    Identification Identification
    Career string
    Responsibility interface {}
}

type Identification struct {
    Phone bool `json:"phone"`
    Email bool `json:"email"`
}

func main() {
    var jsonBlob = []byte(`[
        {
            "ID":1,
            "Name":"Tony",
            "Career":"Engineer",
            "Responsibility":{
                "skill":"PHP&Golang&Network",
                "description":"coding"
            }
        },
        {
            "ID":2,
            "Name":"Jim",
            "Career":"Manager",
            "Responsibility":{
                "experienced":true
            }
        }
    ]`)
    
    var users []User
    if err := json.Unmarshal(jsonBlob, &users); err != nil {
        fmt.Println("error:", err)
    }
    fmt.Printf("%#v\n", users)
    fmt.Println(users[0].Responsibility.(map[string]interface{})["description"].(string))
    fmt.Println(users[1].Responsibility.(map[string]interface{})["experienced"].(bool))
}

雖然可利用上述方式,但是 code 實在不是很美觀,尤其是在讀出資料的時候

利用 *json.RawMessage 延遲解析

上述結構內的 Responsibility 實際上是在解析 Career 才會知道它的資料結構,這時候就可以使用 json.RawMessage 來延遲解析,讓數據以 byte 的方式繼續存在,等待下一次的解析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    ID int
    Name string
    Money float64 `json:",string"`
    Skills []string
    Relationship map[string]string
    Identification Identification
    Career string
    Responsibility json.RawMessage
}

type Identification struct {
    Phone bool `json:"phone"`
    Email bool `json:"email"`
}

type Engineer struct {
    Skill string `json:"skill"`
    Description string `json:"description"`
}

type Manager struct {
    Experienced bool `json:"experienced"`
}

func main() {
    var jsonBlob = []byte(`[
        {
            "ID":1,
            "Name":"Tony",
            "Career":"Engineer",
            "Responsibility":{
                "skill":"PHP&Golang&Network",
                "description":"coding"
            }
        },
        {
            "ID":2,
            "Name":"Jim",
            "Career":"Manager",
            "Responsibility":{
                "experienced":true
            }
        }
    ]`)
    
    var users []User
    if err := json.Unmarshal(jsonBlob, &users); err != nil {
        fmt.Println("error:", err)
    }

    for _, user := range users {
        switch user.Career {
        case "Engineer":
            var responsibility Engineer
            if err := json.Unmarshal(user.Responsibility, &responsibility); err != nil {
                fmt.Println("error:", err)
            }
            fmt.Println(responsibility.Description)
        case "Manager":
            var responsibility Manager
            if err := json.Unmarshal(user.Responsibility, &responsibility); err != nil {
                fmt.Println("error:", err)
            }
            fmt.Println(responsibility.Experienced)
        default:
            fmt.Println("warning:", "don't exist")
        }
    }
}

Tony
作者
Tony
Software Engineer