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