Golang 處理 Json(二):解碼


golang 編碼 json 還比較簡單,而解析 json 則非常蛋疼。不像 PHP 一句 json_decode() 就能搞定。之前項目開發中,為了兼容不同客戶端的需求,請求的 content-type 可以是 json,也可以是 www-x-urlencode。然后某天前端希望某個后端服務提供 json 的處理,而當時后端使用 java 實現了 www-x-urlencode 的請求,對於突然希望提供 json 處理產生了極大的情緒。當時不太理解,現在看來,對於靜態語言解析未知的 JSON 確實是一項挑戰。

定義結構

與編碼 json 的 Marshal 類似,解析 json 也提供了 Unmarshal 方法。對於解析 json,也大致分兩步,首先定義結構,然后調用 Unmarshal 方法序列化。我們先從簡單的例子開始吧。

type Account struct {
	Email    string  `json:"email"`
	Password string  `json:"password"`
	Money    float64 `json:"money"`
}

var jsonString string = `{
	"email": "phpgo@163.com",
	"password" : "123456",
	"money" : 100.5
}`

func main() {

	account := Account{}
	err := json.Unmarshal([]byte(jsonString), &account)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%+v\n", account)
}

輸出:

{Email:phpgo@163.com Password:123456 Money:100.5}

Unmarshal 接受一個 byte 數組和空接口指針的參數。和 sql 中讀取數據類似,先定義一個數據實例,然后傳其指針地址。

與編碼類似,golang 會將 json 的數據結構和 go 的數據結構進行匹配。匹配的原則就是尋找 tag 的相同的字段,然后查找字段。查詢的時候是 大小寫不敏感的

type Account struct {
	Email    string  `json:"email"`
	PassWord string
	Money    float64 `json:"money"`
}

var jsonString string = `{
	"email": "phpgo@163.com",
	"password" : "123456",
	"money" : 100.5
}`

func main() {

	account := Account{}
	err := json.Unmarshal([]byte(jsonString), &account)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%+v\n", account)
}

輸出:

{Email:phpgo@163.com PassWord:123456 Money:100.5}

把 Password 的 tag 去掉,再修改成 PassWord,依然可以把 json 的 password 匹配到 PassWord,但是如果結構的字段是私有的,即使 tag 符合,也不會被解析:

type Account struct {
	Email    string  `json:"email"`
	password string   `json:"password"`
	Money    float64 `json:"money"`
}

var jsonString string = `{
	"email": "phpgo@163.com",
	"password" : "123456",
	"money" : 100.5
}`

func main() {

	account := Account{}
	err := json.Unmarshal([]byte(jsonString), &account)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%+v\n", account)
}

輸出:

{Email:phpgo@163.com password: Money:100.5}

上面的 password 並不會被解析賦值 json 的 password,大小寫不敏感只是針對公有字段而言。

再尋找 tag 或字段的時候匹配不成功,則會拋棄這個 json 字段的值:

type Account struct {
	Email    string  `json:"email"`
	Password string   `json:"password"`
}

var jsonString string = `{
	"email": "phpgo@163.com",
	"password" : "123456",
	"money" : 100.5
}`

func main() {

	account := Account{}
	err := json.Unmarshal([]byte(jsonString), &account)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%+v\n", account)
}

輸出:

{Email:phpgo@163.com Password:123456}

並不會有money字段被賦值。

string tag

在編碼的時候,我們使用 tag string,可以把結構定義的數字類型以字串形式編碼。同樣在解碼的時候,只有字串類型的數字,才能被正確解析,否則會報錯:

type Account struct {
	Email    string  `json:"email"`
	Password string  `json:"password"`
	Money    float64 `json:"money,string"`
}

var jsonString string = `{
	"email": "phpgo@163.com",
	"password" : "123456",
	"money" : "100.5" // 不能沒有 雙引號,否則會報錯
}`

func main() {

	account := Account{}
	err := json.Unmarshal([]byte(jsonString), &account)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%+v\n", account)
}

輸出:

{Email:phpgo@163.com Password:123456 Money:100.5}

Money 是 float64 類型。

如果 json 的 money 是 100.5, 會得到下面的錯誤:

2017/03/08 17:39:21 json: invalid use of ,string struct tag, trying to unmarshal unquoted value into float64
exit status 1

- tag

與編碼一樣,tag 的-也不會被解析,但是會初始化其 零值

type Account struct {
	Email    string  `json:"email"`
	Password string  `json:"password"`
	Money    float64 `json:"-"`
}

var jsonString string = `{
	"email": "phpgo@163.com",
	"password" : "123456",
	"money" : 100.5
}`

func main() {

	account := Account{}
	err := json.Unmarshal([]byte(jsonString), &account)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%+v\n", account)
}

輸出:

{Email:phpgo@163.com Password:123456 Money:0}

稍微總結一下,解析 json 最好的方式就是定義與將要被解析 json 的結構。有人寫了一個小工具 json-to-go,自動將 json 格式化成 golang 的結構。

動態解析

通常根據 json 的格式預先定義 golang 的結構進行解析是最理想的情況。可是實際開發中,理想的情況往往都存在理想的願望之中,很多 json 非但格式不確定,有的還可能是動態數據類型。

例如通常登錄的時候,往往既可以使用手機號做用戶名,也可以使用郵件做用戶名,客戶端傳的 json 可以是字串,也可以是數字。此時服務端解析就需要技巧了。

Decode

前面我們使用了簡單的方法 Unmarshal 直接解析 json 字串,下面我們使用更底層的方法 NewDecode 和 Decode 方法。

package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"strings"
)

type User struct {
	UserName string `json:"username"`
	Password string `json:"password"`
}

var jsonString string = `{
	"username": "phpgo@163.com",
	"password": "123"
}`

func Decode(r io.Reader) (u *User, err error) {
	u = new(User)
	err = json.NewDecoder(r).Decode(u)
	if err != nil {
		return
	}
	return
}

func main() {
	user, err := Decode(strings.NewReader(jsonString))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%#v\n", user)
}

輸出:

&main.User{UserName:"phpgo@163.com", Password:"123"}

我們定義了一個 Decode 函數,在這個函數進行 json 字串的解析。然后調用 json 的 NewDecoder 方法構造一個 Decode 對象,最后使用這個對象的 Decode 方法賦值給定義好的結構對象。

對於字串,可是使用 strings.NewReader 方法,讓字串變成一個 Stream 對象。

接口

如果客戶端傳的 username 的值是一個數字類型的手機號,那么上面的解析方法將會失敗。正如我們之前所介紹的動態類型行為一樣,使用空接口可以 hold 住這樣的情景。

 

type User struct {
	UserName interface{} `json:"username"`
	Password string `json:"password"`
}

var jsonString string = `{
	"username": 15899758289,
	"password": "123"
}`

func Decode(r io.Reader) (u *User, err error) {
	u = new(User)
	err = json.NewDecoder(r).Decode(u)
	if err != nil {
		return
	}
	return
}

func main() {
	user, err := Decode(strings.NewReader(jsonString))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%#v\n", user)
}

輸出:

&main.User{UserName:1.5899758289e+10, Password:"123"}

貌似成功了,可是返回的數字是科學計數法,有點奇怪。可以使用 golang 的斷言,然后轉換類型:

type User struct {
	UserName interface{} `json:"username"`
	Password string `json:"password"`
}

var jsonString string = `{
	"username": 15899758289,
	"password": "123"
}`

func Decode(r io.Reader) (u *User, err error) {
	u = new(User)
	if err = json.NewDecoder(r).Decode(u); err != nil {
		return
	}

	switch t := u.UserName.(type) {
	case string:
		u.UserName = t
	case float64:
		u.UserName = int64(t)
	}

	return
}

func main() {
	user, err := Decode(strings.NewReader(jsonString))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%#v\n", user)
}

輸出:

&main.User{UserName:15899758289, Password:"123"}

看起來挺好,可是我們的 UserName 字段始終是一個空接口,使用他的時候,還是需要轉換類型,這樣情況看來,解析的時候就應該轉換好類型,那么用的時候就省心了。

修改定義的結構如下:

type User struct {
	UserName interface{} `json:"username"`
	Password string `json:"password"`

	Email string
	Phone int64
}

這樣就能通過 fmt.Println(user.Email + " add me") 使用字段進行操作了。當然也有人認為 Email 和 Phone 純粹多於,因為使用的時候,還是需要再判斷當前結構實例是那種情況。

延遲解析

因為 UserName 字段,實際上是在使用的時候,才會用到他的具體類型,因此我們可以延遲解析。使用 json.RawMessage 方式,將 json 的字串繼續以 byte 數組方式存在。

type User struct {
	UserName json.RawMessage `json:"username"`
	Password string `json:"password"`

	Email string
	Phone int64
}

var jsonString string = `{
	"username": "phpgo@163.com",
	"password": "123"
}`

func Decode(r io.Reader) (u *User, err error) {
	u = new(User)
	if err = json.NewDecoder(r).Decode(u); err != nil {
		return
	}

	var email string
	if err = json.Unmarshal(u.UserName, &email); err == nil {
		u.Email = email
		return
	}

	var phone int64
	if err = json.Unmarshal(u.UserName, &phone); err == nil {
		u.Phone = phone
	}

	return
}

func main() {
	user, err := Decode(strings.NewReader(jsonString))
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%#v\n", user)
}

總體而言,延遲解析和使用空接口的方式類似。需要再次調用 Unmarshal 方法,對 json.RawMessage 進行解析。原理和解析到接口的形式類似。

不定字段解析

對於未知 json 結構的解析,不同的數據類型可以映射到接口或者使用延遲解析。有時候,會遇到 json 的數據字段都不一樣的情況。例如需要解析下面一個 json 字串:

接口配合斷言

var jsonString string = `{
        "things": [
            {
                "name": "Alice",
                "age": 37
            },
            {
                "city": "Ipoh",
                "country": "Malaysia"
            },
            {
                "name": "Bob",
                "age": 36
            },
            {
                "city": "Northampton",
                "country": "England"
            }
        ]
    }`

json 字串的是一個對象,其中一個 key things 的值是一個數組,這個數組的每一個 item 都未必一樣,大致是兩種數據結構,可以抽象為 person 和 place。即,定義下面的結構體:

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

type Place struct {
    City    string `json:"city"`
    Country string `json:"country"`
}

接下來我們 Unmarshal json 字串到一個 map 結構,然后迭代 item 並使用 type 斷言的方式解析數據:

func decode(jsonStr []byte) (persons []Person, places []Place) {
    var data map[string][]map[string]interface{}
    err := json.Unmarshal(jsonStr, &data)
    if err != nil {
        fmt.Println(err)
        return
    }

    for i := range data["things"] {
        item := data["things"][i]
        if item["name"] != nil {
            persons = addPerson(persons, item)
        } else {
            places = addPlace(places, item)
        }

    }
    return
}

迭代的時候會判斷 item 是否是 person 還是 place,然后調用對應的解析方法:

func addPerson(persons []Person, item map[string]interface{}) []Person {
	name := item["name"].(string)
	age := item["age"].(float64)
	person := Person{name, int(age)}
	persons = append(persons, person)

	return persons
}

func addPlace(places []Place, item map[string]interface{}) []Place {
	city := item["city"].(string)
	country := item["country"].(string)
	place := Place{City: city, Country: country}
	places = append(places, place)

	return places
}

代碼匯總:

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

type Place struct {
	City    string `json:"city"`
	Country string `json:"country"`
}

func decode(jsonStr []byte) (persons []Person, places []Place) {
	var data map[string][]map[string]interface{}

	err := json.Unmarshal(jsonStr, &data)
	if err != nil {
		fmt.Println(err)
		return
	}

	for i := range data["things"] {
		item := data["things"][i]
		if item["name"] != nil {
			persons = addPerson(persons, item)
		} else {
			places = addPlace(places, item)
		}
	}

	return
}

func addPerson(persons []Person, item map[string]interface{}) []Person {
	name := item["name"].(string)
	age := item["age"].(float64)
	person := Person{name, int(age)}
	persons = append(persons, person)

	return persons
}

func addPlace(places []Place, item map[string]interface{}) []Place {
	city := item["city"].(string)
	country := item["country"].(string)
	place := Place{City: city, Country: country}
	places = append(places, place)

	return places
}

var jsonString string = `{
	"things": [
		{
			"name": "Alice",
			"age": 37
		},
		{
			"city": "Ipoh",
			"country": "Malaysia"
		},
		{
			"name": "Bob",
			"age": 36
		},
		{
			"city": "Northampton",
			"country": "England"
		}
	]
}`

func main() {
	personA, placeA := decode([]byte(jsonString))

	fmt.Printf("%+v\n", personA)
	fmt.Printf("%+v\n", placeA)
}

輸出:

[{Name:Alice Age:37} {Name:Bob Age:36}]
[{City:Ipoh Country:Malaysia} {City:Northampton Country:England}]

混合結構

混合結構很好理解,如同我們前面解析 username 為 email 和 phone 兩種情況,就在結構中定義好這兩種結構即可。

type Mixed struct {
	Name    string `json:"name"`
	Age     int `json:"age"`
	city    string `json:"city"`
	Country string  `json:"country"`
}

混合結構的思路很簡單,借助 golang 會初始化沒有匹配的 json 和拋棄沒有匹配的 json,給特定的字段賦值。比如每一個 item 都具有四個字段,只不過有的會匹配 person 的 json 數據,有的則是匹配 place。沒有匹配的字段則是零值。接下來在根據 item 的具體情況,分別賦值到對於的 Person 或 Place 結構。

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

type Place struct {
	City    string `json:"city"`
	Country string `json:"country"`
}

type Mixed struct {
	Name    string `json:"name"`
	Age     int `json:"age"`
	city    string `json:"city"`
	Country string  `json:"country"`
}

func decode(jsonStr []byte) (persons []Person, places []Place) {
	var data map[string][]Mixed

	err := json.Unmarshal(jsonStr, &data)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Printf("%+v\n", data["things"])

	for i := range data["things"] {
		item := data["things"][i]
		if item.Name != "" {
			persons = append(persons, Person{Name: item.Name, Age: item.Age})
		} else {
			places = append(places, Place{City: item.city, Country:item.Country})
		}
	}

	return
}

var jsonString string = `{
	"things": [
		{
			"name": "Alice",
			"age": 37
		},
		{
			"city": "Ipoh",
			"country": "Malaysia"
		},
		{
			"name": "Bob",
			"age": 36
		},
		{
			"city": "Northampton",
			"country": "England"
		}
	]
}`

func main() {
	personA, placeA := decode([]byte(jsonString))

	fmt.Printf("%+v\n", personA)
	fmt.Printf("%+v\n", placeA)
}

輸出:

[{Name:Alice Age:37 city: Country:} {Name: Age:0 city: Country:Malaysia} {Name:Bob Age:36 city: Country:} {Name: Age:0 city: Country:England}]
[{Name:Alice Age:37} {Name:Bob Age:36}]
[{City: Country:Malaysia} {City: Country:England}]

混合結構的解析方式也很不錯。思路還是借助了解析 json 中拋棄不要的字段,借助零值處理。

json.RawMessage

json.RawMessage 非常有用,延遲解析也可以使用這個樣例。我們已經介紹過類似的技巧,下面就貼代碼了:

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

type Place struct {
	City    string `json:"city"`
	Country string `json:"country"`
}

func addPerson(item json.RawMessage, persons []Person) ([]Person) {
	person := Person{}
	if err := json.Unmarshal(item, &person); err != nil {
		fmt.Println(err)
	} else {
		if person != *new(Person) {
			persons = append(persons, person)
		}
	}

	return persons
}

func addPlace(item json.RawMessage, places []Place) ([]Place) {
	place := Place{}
	if err := json.Unmarshal(item, &place); err != nil {
		fmt.Println(err)
	} else {
		if place != *new(Place) {
			places = append(places, place)
		}
	}

	return places
}

func decode(jsonStr []byte) (persons []Person, places []Place) {
	var data map[string][]json.RawMessage

	err := json.Unmarshal(jsonStr, &data)
	if err != nil {
		fmt.Println(err)
		return
	}

	for _, item := range data["things"] {
		persons = addPerson(item, persons)
		places = addPlace(item, places)
	}

	return
}

var jsonString string = `{
	"things": [
		{
			"name": "Alice",
			"age": 37
		},
		{
			"city": "Ipoh",
			"country": "Malaysia"
		},
		{
			"name": "Bob",
			"age": 36
		},
		{
			"city": "Northampton",
			"country": "England"
		}
	]
}`

func main() {
	personA, placeA := decode([]byte(jsonString))

	fmt.Printf("%+v\n", personA)
	fmt.Printf("%+v\n", placeA)
}

輸出:

[{Name:Alice Age:37} {Name:Bob Age:36}]
[{City:Ipoh Country:Malaysia} {City:Northampton Country:England}]

把 things 的 item 數組解析成一個 json.RawMessage,然后再定義其他結構逐步解析。上述這些例子其實在真實的開發環境下,應該盡量避免。像 person 或是 place 這樣的數據,可以定義兩個數組分別存儲他們,這樣就方便很多。不管怎么樣,通過這個略傻的例子,我們也知道了如何解析 json 數據。

總結

關於 golang 解析 json 的介紹基本就這么多。想要解析越簡單,就需要定義越明確的 map 結構。面對無法確定的數據結構或類型,再動態解析方面可以借助接口與斷言的方式解析,也可以使 json.RawMessage 延遲解析。具體使用情況,還得考慮實際的需求和應用場景。

總而言之,使用 json 作為現在 api 的數據通信方式已經很普遍了。

 

 

相關文章

Golang 處理 Json(一):編碼

Golang 處理 Json(二):解碼

 

 

參考:

http://json.org/

http://www.jianshu.com/p/31757e530144

https://golang.org/pkg/encoding/json/ 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM