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 stringMoney 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 mainimport ("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 = tcase 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 stringPhone 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 stringPhone 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 stringif err = json.Unmarshal(u.UserName, &email); err == nil {u.Email = emailreturn}var phone int64if 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][]Mixederr := 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.RawMessageerr := 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/?