Golang处理JSON(二)--- 解码

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纯粹多于,因为使用的时候,还是需要再判断当前结构实例是那种情况。

延迟解析

因为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":"18512341234@qq.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.Fatalln(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
}

最后调用和输出如下:

func main() {
    personsA, placesA := decode([]byte(jsonString))
    fmt.Printf("%+v\n", personsA)
    fmt.Printf("%+v\n", placesA)
}


/usr/local/go/bin/go run /Users/ghost/Rsj217/go/src/demo/main.go
[{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结构。

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
}

混合结构的解析方式也很不错。思路还是借助了解析json中抛弃不要的字段,借助零值处理。

json.RawMessage

json.RawMessage非常有用,延迟解析也可以使用这个样例。我们已经介绍过类似的技巧,下面就贴代码了:


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
}

把things的item数组解析成一个json.RawMessage,然后再定义其他结构逐步解析。上述这些例子其实在真实的开发环境下,应该尽量避免。像person或是place这样的数据,可以定义两个数组分别存储他们,这样就方便很多。不管怎么样,通过这个略傻的例子,我们也知道了如何解析json数据。

总结

关于golang解析json的介绍基本就这么多。想要解析越简单,就需要定义越明确的map结构。面对无法确定的数据结构或类型,再动态解析方面可以借助接口与断言的方式解析,也可以使用json.RawMessage延迟解析。具体使用情况,还得考虑实际的需求和应用场景。

总而言之,使用json作为现在api的数据通信方式已经很普遍了。我们从启动服务,构造请求,解析请求,渲染模板,持久化到现在的json解析,涉及一个request-response生命周期的介绍完整了。

接下来自然就是利用这些,构建一个简单的web应用。当然,我们可能根据需要,构建不同的应用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 140,386评论 1 295
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 60,319评论 1 254
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 92,325评论 0 206
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 40,558评论 0 171
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 48,200评论 1 249
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 38,496评论 1 167
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,191评论 2 265
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 28,971评论 0 161
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 28,688评论 6 223
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 32,330评论 0 211
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,098评论 2 211
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 30,412评论 1 222
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 24,116评论 0 31
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 26,937评论 2 206
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 31,314评论 3 200
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,558评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 25,879评论 0 162
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 33,275评论 2 226
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 33,386评论 2 228

推荐阅读更多精彩内容

  • JSON http的交互的生命周期包含请求和响应。前面我们介绍了很多关于发起请求,处理请求的内容。现在该聊一聊返回...
    人世间阅读 37,056评论 4 41
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 133,983评论 18 139
  • 点击查看原文 Web SDK 开发手册 SDK 概述 网易云信 SDK 为 Web 应用提供一个完善的 IM 系统...
    layjoy阅读 13,022评论 0 15
  • 1.标题要激发用户好奇 (1)提出疑问 (2)颠覆认知 (3)吊胃口 2.标题要引发用户共鸣 替用户说出他最想说的...
    别样的人生阅读 339评论 0 1
  • 最后一片叶子落下 也是另一种风景 开学了,平凡和其他人一样,背着新买的书包穿着新...
    陈哇噻aa阅读 889评论 0 9