zoukankan      html  css  js  c++  java
  • Go 的json 解析标准库竟然存在这样的陷阱?

    日常工作中,最常用的数据传输格式就是json,而encoding/json库是内置做解析的库。这一节来看看它的用法,还有几个日常使用中隐晦的陷阱和处理技巧。

    • json 与 struct

    • 解析

    • 反解析

    • 陷阱 1、忘记取地址

    • 陷阱 2、大小写

    • 陷阱 3、十六进制或其他非 UTF8 字符串

    • 陷阱 4、数字转 interface{}

    • 神技、版本变更兼容

    • 小结

    json 与 struct

    一个常见的接口返回内容如下:

    {
      "data": {
        "items": [
          {
            "_id": 2
          }
        ],
        "total_count": 1
      },
      "message": "",
      "result_code": 200
    }

    golang中往往是要把json格式转换成结构体对象使用的。

    在新版Goland粘贴json会自动生成结构体,也可以在网上搜到现成的工具完成自动转换。

    type ResponseData struct {
     Data struct {
      Items []struct {
       Id int `json:"_id"`
      } `json:"items"`
      TotalCount int `json:"total_count"`
     } `json:"data"`
     Message    string `json:"message"`
     ResultCode int    `json:"result_code"`
    }

    用反斜杠加注解的方式表明属于json中哪个字段,要注意不应该嵌套层数过多,否则难以阅读容易出错。

    一般把内部结构体提出来,方便其他业务另做他用。

    type ResponseData struct {
     Data struct {
      Items []Body `json:"items"`
      TotalCount int64 `json:"total_count"`
     } `json:"data"`
     Message    string `json:"message"`
     ResultCode int64  `json:"result_code"`
    }

    type Body struct {
     ID int `json:"_id"`
    }

    解析

    解析就是把json字符串转成struct类型。如下,第一个参数为字节数组,第二个为接收的结构体实体地址。如有报错返回错误信息,如没有返回nil

    //函数签名
    func Unmarshal(data []byte, v interface{}) error
    // 用法
    err := json.Unmarshal([]byte(jsonStr), &responseData)

    完整代码如下

    func foo() {
     jsonStr := `{"data":{"items":[{"_id":2}],"total_count":1},"message":"","result_code":200}`
     //把string解析成struct
     var responseData ResponseData
     err := json.Unmarshal([]byte(jsonStr), &responseData)
     if err != nil {
      fmt.Println("parseJson error:" + err.Error())
      return
     }
     fmt.Println(responseData)
    }

    输出如下,和javatoString不同,go会直接输出了值,如有需要要自行实现并绑定ToString方法。

    {{[{2}] 1}  200}

    反解析

    第一步,复习初始化结构体的方法。

    r := ResponseData{
        Data: struct {
            Items      []Body `json:"items"`
            TotalCount int64  `json:"total_count"`
        }{
            Items: []Body{
                {ID: 1},
                {ID: 2},
            },
            TotalCount: 1,
        },
        Message:    "",
        ResultCode: 200,
    }

    如上,无类型的结构体Data需要明确把类型再写一遍,再为其赋值。[]Body因为是列表类型,内部如上赋值即可。

    反解析函数签名如下,传入结构体,返回编码好的[]byte,和可能的报错信息。

    func Marshal(v interface{}) ([]byte, error)

    完整代码如下

    func bar() {
     r := ResponseData{
      ....
     }
     //把struct编译成string
     resBytes, err := json.Marshal(r)
     if err != nil {
      fmt.Println("convertJson error: " + err.Error())
     }
     fmt.Println(string(resBytes))
    }

    输出

    {"data":{"items":[{"_id":1},{"_id":2}],"total_count":1},"message":"","result_code":200}

    陷阱 1、忘记取地址

    解析的代码在结尾处应该是&responseData) 忘记取地址会导致无法赋值成功,返回报错。

    err := json.Unmarshal([]byte(jsonStr), responseData)

    输出报错

    json: Unmarshal(non-pointer main.ResponseData)

    陷阱 2、大小写

    定义一个简单的结构体来演示这个陷阱。

    type People struct {
     Name string `json:"name"`
     age  int    `json:"age"`
    }

    变量如果需要被外部使用,也就是java中的public权限,定义时首字母必须用大写,这也是Go约定的权限控制。

    type People struct

    要用来解析jsonstruct内部,假如使用了小写作为变量名,会导致无法解析成功,而且不会报错!

    func err1() {
     reqJson := `{"name":"minibear2333","age":26}`
     var person People
     err := json.Unmarshal([]byte(reqJson), &person)
     if err != nil {...}
     fmt.Println(person)
    }

    输出 0,没有成功取到age字段。

    {minibear2333 0}

    这是因为标准库中是使用反射来获取的,私有字段是无法获取到的,源码内部不知道有这个字段,自然无法显示报错信息。

    我以前没有用自动解析,手敲上去结构体,很容易出现这样的问题,把某个字段首字母弄成小写。好在编译器会有提示。

    图片

    陷阱 3、十六进制或其他非 UTF8 字符串

    Go 默认使用的字符串编码是 UTF8 编码的。直接解析会出错

    func err2() {
     raw := []byte(`{"name":"xc2"}`)
     var person People
     if err := json.Unmarshal(raw, &person); err != nil {
      fmt.Println(err)
     }
    }

    输出

    invalid character 'x' in string escape code

    要特别注意,加上反斜杠转义可以成功,或者使用base64编码成字符串,这下子单元测试的重要性就体现出来了。如下:

    raw := []byte(`{"name":"\xc2"}`)
    raw := []byte(`{"name":"wg=="}`)

    其他需要注意的是编码如果不是UTF-8格式,那么Go会用  (U+FFFD) 来代替无效的 UTF8,这不会报错,但是获得的字符串可能不是你需要的结果。

    陷阱 4、数字转 interface{}

    因为默认编码无类型数字视为 float64 。如果想用类型判断语句为int会直接panic

    func err4() {
     var data = []byte(`{"age": 26}`)
     var result map[string]interface{}
     ...
     var status = result["age"].(int) //error
    }
    • 上面的代码隐含一个知识点,jsonvalue是简单类型时,可以直接解析成字典。
    • 如果有嵌套,那么内部类型也会解析成字典。
    • 解析成字典,输出的时候有类似ToString的效果。

    运行时 Panic:

    panic: interface conversion: interface {} is float64, not int

    goroutine 1 [running]:
    main.err4()
    • 可以先转换成float64再转换成int
    • 其实还有几种方法,太麻烦了也没有必要,就不做特别介绍了。

    神技、版本变更兼容

    你有没有遇到过一种场景,一个接口更新了版本,把json的某个字段变更了,在请求的时候每次都定义两套struct

    比如Age在版本 1 中是int在版本 2 中是string,解析的过程中就会出错。

    json: cannot unmarshal number into Go struct field People.age of type string

    我在下面介绍一个技巧,可以省去每次解析都要转换的工作。

    我在源码里面看到,无论反射获得的是哪种类型都会去调用相应的解析接口UnmarshalJSON

    结合前面的知识,在Go里面看起来像鸭子就是鸭子,我们只要实现这个方法,并绑定到结构体对象上,就可以让源码来调用我们的方法。

    type People struct {
        Name string `json:"name"`
        Age  int    `json:"_"`
    }
    func (p *People) UnmarshalJSON(b []byte) error {
     ...
    }
    • 使用下划线表示此类型不解析。
    • 必须用指针的方式绑定方法。
    • 必须与 interface{}中定义的方法签名完全一致。

    一共有四个步骤

    1、定义临时类型。用来接受非json:"_"的字段,注意用的是type关键字。

    type tmp People

    2、用中间变量接收 json 串,tmp 以外的字段用来接受json:"_"属性字段

    var s = &struct {
        tmp
        // interface{}类型,这样才可以接收任意字段
        Age interface{} `json:"age"`
    }{}
    // 解析
    err := json.Unmarshal(b, &s)

    3、判断真实类型,并类型转换

    switch t := s.Age.(type) {
    case string:
        var age int
        age, err = strconv.Atoi(t)
        if err != nil {...}
        s.tmp.Age = age
    case float64:
        s.tmp.Age = int(t)
    }

    4、tmp 类型转换回 People,并赋值

    *p = People(s.tmp)

    小结

    通过本节,我们掌握了标准库中json解析和反解析的方法,以及很有可能日常工作中踩到的几个坑。它们是:

    • 陷阱 1、忘记取地址
    • 陷阱 2、大小写
    • 陷阱 3、十六进制或其他非 UTF8 字符串
    • 陷阱 4、数字转 interface{}
    • 版本变量时兼容技巧
  • 相关阅读:
    IDEA使用笔记
    面试题整理
    java中的hashCode()方法
    动态规划算法实现部分——0/1背包问题
    算法课堂笔记6—近似算法
    Photoshop CC 2015
    unity学习笔记1--Space Shooter
    转载---sql之left join、right join、inner join的区别
    java连接Fastdfs图片服务器上传失败的解决方法
    Eclipse使用笔记
  • 原文地址:https://www.cnblogs.com/kcxg/p/15107746.html
Copyright © 2011-2022 走看看