golang编码json还比较简单,而解析json则非常蛋疼。不像Python一句json.loads就能搞定。之前项目开发中,为了兼容不同客户端的需求,请求的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":"rsj217@gmail.com", "password":"123", "money":100.5 }` func main() { account := Account{} err := json.Unmarshal([]byte(jsonString), &account) if err != nil{ log.Fatalln(err) } fmt.Printf("%+v\n", account) }Unmarshal接受一个byte数组和空接口指针的参数。和sql中读取数据类似,先定义一个数据实例,然后传其指针地址。
与编码类似,golang会将json的数据结构和go的数据结构进行匹配。匹配的原则就是寻找tag的相同的字段,然后查找字段。查询的时候是大小写不敏感的:
type Account struct { Email string `json:"email"` PassWord string Money float64 `json:"money"` }输出{Email:rsj217@gmail.com PassWord:123 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"` }输出{Email:rsj217@gmail.com password: Money:100.5}。上面的password并不会被解析赋值json的password,大小写不敏感只是针对公有字段而言。再寻找tag或字段的时候匹配不成功,则会抛弃这个json字段的值:
type Account struct { Email string `json:"email"` Password string `json:"password"` }输出 {Email:rsj217@gmail.com Password:"123"}, 并不会有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":"rsj217@gmail.com", "password":"123", "money":"100.5" }` func main() { account := Account{} err := json.Unmarshal([]byte(jsonString), &account) if err != nil{ log.Fatalln(err) } fmt.Printf("%+v\n", account) }输出: {Email:rsj217@gmail.com Password:123 Money:100.5}, Money是float64类型。
如果json的money是100.5, 会得到下面的错误:
2016/12/23 18:12:32 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:"-"` }输出:{Email:rsj217@gmail.com Password:123 Money:0}。
稍微总结一下,解析json最好的方式就是定义与将要被解析json的结构。有人写了一个小工具json-to-go,自动将json格式化成golang的结构。
动态解析通常更加json的格式预先定义golang的结构进行解析是最理想的情况。可是实际开发中,理想的情况往往都存在理想的愿望之中,很多json非但格式不确定,有的还可能是动态数据类型。
例如通常登录的时候,往往既可以使用手机号做用户名,也可以使用邮件做用户名,客户端传的json可以是字串,也可以是数字。此时服务端解析就需要技巧了。
Decode前面我们使用了简单的方法Unmarshal直接解析json字串,下面我们使用更底层的方法NewDecode和Decode方法。
type User struct { UserName string `json:"username"` Password string `json:"password"` } var jsonString string = `{ "username":"rsj217@gmail.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.Fatalln(err) } fmt.Printf("%#v\n",user) }我们定义了一个Decode函数,在这个函数进行json字串的解析。然后调用json的NewDecoder方法构造一个Decode对象,最后使用这个对象的Decode方法赋值给定义好的结构对象。
对于字串,可是使用strings.NewReader方法,让字串变成一个Stream对象。
接口如果客户端传的username的值是一个数字类型的手机号,那么上面的解析方法将会失败。正如我们之前所介绍的动态类型行为一样,使用空接口可以hold住这样的情景。
type User struct { UserName interface{} `json:"username"` Password string `json:"password"` }然后再运行发送输出为 &main.User{UserName:1.8512341234e+10, Password:"123"}。怎么说,貌似成功了,可是返回的数字是科学计数法,有点奇怪。可以使用golang的断言,然后转换类型:
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.Fatalln(err) } fmt.Printf("%#v\n", user) }输出 &main.User{UserName:18512341234, 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纯粹多于,因为使用的时候,还是需要再判断当前结构实例是那种情况。
延迟解析