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 的數據通信方式已經很普遍了。
相關文章
參考:
