Golang中遇到的一些关于JSON处理的坑

1,309
原文链接: ethancai.github.io

前言

一个人不会两次掉进同一个坑里,但是如果他(她)忘记了坑的位置,那就不一定了。

这篇文章记录了最近使用Golang处理JSON遇到的一些坑。

1号坑:omitempty的行为

C#中最常用的JSON序列化类库Newtonsoft.Json中,把一个类的实例序列化成JSON,如果我们不想让某个属性输出到JSON中,可以通过property annotation或者ShouldSerialize method等方法,告知序列化程序。如下:

// 通过ShouldSerialize method指示不要序列化ObsoleteSetting属性
class Config
{
    public Fizz ObsoleteSetting { get; set; }

    public bool ShouldSerializeObsoleteSetting()
    {
        return false;
    }
}

// 通过JsonIgnore的annotation指示不需要序列化ObsoleteSetting属性
class Config
{
    [JsonIgnore]
    public Fizz ObsoleteSetting { get; set; }

    public Bang ReplacementSetting { get; set; }
}

关于Newtonsoft.Json的Conditional Property Serialization的更多内容参考:

开始使用Golang的时候,以为omitempty的行为和C#中一样用来控制是否序列化字段,结果使用的时候碰了一头钉子。回头阅读encoding/json package的官方文档,找到对omitempty行为的描述:

Struct values encode as JSON objects. Each exported struct field becomes a member of the object unless

  • the field’s tag is “-“, or
  • the field is empty and its tag specifies the “omitempty” option.

The empty values are false, 0, any nil pointer or interface value, and any array, slice, map, or string of length zero. The object’s default key string is the struct field name but can be specified in the struct field’s tag value. The “json” key in the struct field’s tag value is the key name, followed by an optional comma and options. Examples:

> // Field is ignored by this package.
> Field int `json:"-"`
>
> // Field appears in JSON as key "myName".
> Field int `json:"myName"`
>
> // Field appears in JSON as key "myName" and
> // the field is omitted from the object if its value is empty,
> // as defined above.
> Field int `json:"myName,omitempty"`
>
> // Field appears in JSON as key "Field" (the default), but
> // the field is skipped if empty.
> // Note the leading comma.
> Field int `json:",omitempty"`
>

Golang中,如果指定一个field序列化成JSON的变量名字为-,则序列化的时候自动忽略这个field。这种用法,才是和上面JsonIgnore的用法的作用是一样的。

omitempty的作用是当一个field的值是empty的时候,序列化JSON时候忽略这个fieldNewtonsoft.Json的类似用法参考这里例子)。这里需要注意的是关于emtpty的定义:

The empty values are false, 0, any nil pointer or interface value, and any array, slice, map, or string of length zero.

通过下面的例子,来加深对empty values的了解:

package main

import (
    "bytes"
    "encoding/json"
    "log"
    "os"
)

type S1 struct {
    I1 int
    I2 int `json:",omitempty"`

    F1 float64
    F2 float64 `json:",omitempty"`

    S1 string
    S2 string `json:",omitempty"`

    B1 bool
    B2 bool `json:",omitempty"`

    Slice1 []int
    Slice2 []int `json:",omitempty"`
    Slice3 []int `json:",omitempty"`

    Map1 map[string]string
    Map2 map[string]string `json:",omitempty"`
    Map3 map[string]string `json:",omitempty"`

    O1 interface{}
    O2 interface{} `json:",omitempty"`
    O3 interface{} `json:",omitempty"`
    O4 interface{} `json:",omitempty"`
    O5 interface{} `json:",omitempty"`
    O6 interface{} `json:",omitempty"`
    O7 interface{} `json:",omitempty"`
    O8 interface{} `json:",omitempty"`

    P1 *int
    P2 *int               `json:",omitempty"`
    P3 *int               `json:",omitempty"`
    P4 *float64           `json:",omitempty"`
    P5 *string            `json:",omitempty"`
    P6 *bool              `json:",omitempty"`
    P7 *[]int             `json:",omitempty"`
    P8 *map[string]string `json:",omitempty"`
}

func main() {

    p3 := 0
    p4 := float64(0)
    p5 := ""
    p6 := false
    p7 := []int{}
    p8 := map[string]string{}

    s1 := S1{
        I1: 0,
        I2: 0,

        F1: 0,
        F2: 0,

        S1: "",
        S2: "",

        B1: false,
        B2: false,

        Slice1: []int{},
        Slice2: nil,
        Slice3: []int{},

        Map1: map[string]string{},
        Map2: nil,
        Map3: map[string]string{},

        O1: nil,
        O2: nil,
        O3: int(0),
        O4: float64(0),
        O5: "",
        O6: false,
        O7: []int{},
        O8: map[string]string{},

        P1: nil,
        P2: nil,
        P3: &p3,
        P4: &p4,
        P5: &p5,
        P6: &p6,
        P7: &p7,
        P8: &p8,
    }

    b, err := json.Marshal(s1)
    if err != nil {
        log.Printf("marshal error: %v", err)
        return
    }

    var out bytes.Buffer
    json.Indent(&out, b, "", "\t")
    out.WriteTo(os.Stdout)
    //Output:
    //{
    //	"I1": 0,
    //	"F1": 0,
    //	"S1": "",
    //	"B1": false,
    //	"Slice1": [],
    //	"Map1": {},
    //	"O1": null,
    //	"O3": 0,
    //	"O4": 0,
    //	"O5": "",
    //	"O6": false,
    //	"O7": [],
    //	"O8": {},
    //	"P1": null,
    //	"P2": 0
    //}%
}

点击这里执行上面的程序

关于empty value的定义,这里面隐藏了一些坑。下面通过一个例子来说明。

假设我们有一个社交类App,通过Restful API形式从服务端获取当前登录用户基本信息及粉丝数量。如果服务端对Response中User对象的定义如下:

type User struct {
    ID        int `json:"id"`                  // 用户id
    // 其它field
    FansCount int `json:"fansCount,omitempty"` // 粉丝数
}

如果正在使用App时一个还没有粉丝的用户,访问Restful API的得到Response如下:

{
    "id": 1000386,
    ...
}

这时候你会发现Response的User对象中没有fansCount,因为fansCount是个int类型且值为0,序列化的时候会被忽略。语义上,User对象中没有fansCount应该理解为粉丝数量未知,而不是没有粉丝

如果我们希望做到能够区分粉丝数未知没有粉丝两种情况,需要修改User的定义:

type User struct {
    ID        int  `json:"id"`                  // 用户id
    // 其它field
    FansCount *int `json:"fansCount,omitempty"` // 粉丝数
}

FansCount修改为指针类型,如果为nil,表示粉丝数未知;如果为整数(包括0),表示粉丝数。

这么修改语义上没有漏洞了,但是代码中要给FansCount赋值的时候却要多一句废话。必须先将从数据源查询出粉丝数赋给一个变量,然后再将变量的指针传给FansCount。代码读起来实在是啰嗦:

// FansCount是int类型时候
user := dataAccess.GetUserInfo(userId)
user.FansCount = dataAccess.GetFansCount(userId)

// FansCount是*int类型的时候
user := dataAccess.GetUserInfo(userId)
fansCount := dataAccess.GetFansCount(userId)
user.FansCount = &fansCount

2号坑:JSON反序列化成interface{}对Number的处理

JSON的规范中,对于数字类型,并不区分是整型还是浮点型。

对于如下JSON文本:

{
    "name": "ethancai",
    "fansCount": 9223372036854775807
}

如果反序列化的时候指定明确的结构体和变量类型

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name      string
    FansCount int64
}

func main() {
    const jsonStream = `
        {"name":"ethancai", "fansCount": 9223372036854775807}
    `
    var user User  // 类型为User
    err := json.Unmarshal([]byte(jsonStream), &user)
    if err != nil {
        fmt.Println("error:", err)
    }

    fmt.Printf("%+v \n", user)
}
// Output:
//  {Name:ethancai FansCount:9223372036854775807}

点击这里执行上面的程序

如果反序列化不指定结构体类型或者变量类型,则JSON中的数字类型,默认被反序列化成float64类型:

package main

import (
    "encoding/json"
    "fmt"
    "reflect"
)

func main() {
    const jsonStream = `
        {"name":"ethancai", "fansCount": 9223372036854775807}
    `
    var user interface{}  // 不指定反序列化的类型
    err := json.Unmarshal([]byte(jsonStream), &user)
    if err != nil {
        fmt.Println("error:", err)
    }
    m := user.(map[string]interface{})

    fansCount := m["fansCount"]

    fmt.Printf("%+v \n", reflect.TypeOf(fansCount).Name())
    fmt.Printf("%+v \n", fansCount.(float64))
}

// Output:
// 	float64
//  	9.223372036854776e+18

点击这里执行上面的程序

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name      string
    FansCount interface{}  // 不指定FansCount变量的类型
}

func main() {
    const jsonStream = `
        {"name":"ethancai", "fansCount": 9223372036854775807}
    `
    var user User
    err := json.Unmarshal([]byte(jsonStream), &user)
    if err != nil {
        fmt.Println("error:", err)
    }

    fmt.Printf("%+v \n", user)
}

// Output:
// 	{Name:ethancai FansCount:9.223372036854776e+18}

点击这里执行上面的程序

从上面的程序可以发现,如果fansCount精度比较高,反序列化成float64类型的数值时存在丢失精度的问题。

如何解决这个问题,先看下面程序:

package main

import (
    "encoding/json"
    "fmt"
    "reflect"
    "strings"
)

func main() {
    const jsonStream = `
        {"name":"ethancai", "fansCount": 9223372036854775807}
    `

    decoder := json.NewDecoder(strings.NewReader(jsonStream))
    decoder.UseNumber()    // UseNumber causes the Decoder to unmarshal a number into an interface{} as a Number instead of as a float64.

    var user interface{}
    if err := decoder.Decode(&user); err != nil {
        fmt.Println("error:", err)
            return
        }

    m := user.(map[string]interface{})
    fansCount := m["fansCount"]
    fmt.Printf("%+v \n", reflect.TypeOf(fansCount).PkgPath() + "." + reflect.TypeOf(fansCount).Name())

     v, err := fansCount.(json.Number).Int64()
    if err != nil {
        fmt.Println("error:", err)
            return
    }
    fmt.Printf("%+v \n", v)
}

// Output:
// 	encoding/json.Number
// 	9223372036854775807

点击这里执行上面的程序

上面的程序,使用了func (*Decoder) UseNumber方法告诉反序列化JSON的数字类型的时候,不要直接转换成float64,而是转换成json.Number类型。json.Number内部实现机制是什么,我们来看看源码:

// A Number represents a JSON number literal.
type Number string

// String returns the literal text of the number.
func (n Number) String() string { return string(n) }

// Float64 returns the number as a float64.
func (n Number) Float64() (float64, error) {
    return strconv.ParseFloat(string(n), 64)
}

// Int64 returns the number as an int64.
func (n Number) Int64() (int64, error) {
    return strconv.ParseInt(string(n), 10, 64)
}

json.Number本质是字符串,反序列化的时候将JSON的数值先转成json.Number,其实是一种延迟处理的手段,待后续逻辑需要时候,再把json.Number转成float64或者int64

对比其它语言,Golang对JSON反序列化处理真是易用性太差(“蛋疼”)。

JavaScript中所有的数值都是双精度浮点数(参考这里),反序列化JSON的时候不用考虑数值类型匹配问题。这里多说两句,JSON的全名JavaScript Object Notation(从名字上就能看出和JavaScript的关系非常紧密),发明人是Douglas Crockford,如果你自称熟悉JavaScript而不知道 Douglas Crockford是谁,就像是自称是苹果粉丝却不知道乔布斯是谁。

C#语言的第三方JSON处理library Json.NET反序列化JSON对数值的处理也比Golang要优雅的多:

using System;
using Newtonsoft.Json;

public class Program
{
    public static void Main()
    {
        string json = @"{
  'Name': 'Ethan',
  'FansCount': 121211,
  'Price': 99.99
}";

        Product m = JsonConvert.DeserializeObject<Product>(json);

        Console.WriteLine(m.FansCount);
        Console.WriteLine(m.FansCount.GetType().FullName);

        Console.WriteLine(m.Price);
        Console.WriteLine(m.Price.GetType().FullName);

    }
}

public class Product
{
    public string Name
    {
        get;
        set;
    }

    public object FansCount
    {
        get;
        set;
    }

    public object Price
    {
        get;
        set;
    }
}

// Output:
//      121211
//      System.Int64
//      99.99
//      System.Double

点击这里执行上面的程序

Json.NET在反序列化的时候自动识别数值是浮点型还是整型,这一点对开发者非常友好。

3号坑:选择什么格式表示日期

JSON的规范中并没有日期类型,不同语言的library对日期序列化的处理也不完全一致:

Go语言:

package main

import (
    "encoding/json"
    "fmt"
    "os"
    "time"
)

func main() {
    type Product struct {
        Name      string
        CreatedAt time.Time
    }
    pdt := Product{
        Name:      "Reds",
        CreatedAt: time.Now(),
    }
    b, err := json.Marshal(pdt)
    if err != nil {
        fmt.Println("error:", err)
    }
    os.Stdout.Write(b)
}
// Output
//      {"Name":"Reds","CreatedAt":"2016-06-27T07:40:54.69292134+08:00"}

JavaScript语言:

➜  ~ node
> var jo = { name: "ethan", createdAt: Date.now() };
undefined
> JSON.stringify(jo)
'{"name":"ethan","createdAt":1466984665633}'

C#语言:

using System;
using Newtonsoft.Json;

public class Program
{
    public static void Main()
    {
        Product product = new Product();
        product.Name = "Apple";
        product.CreatedAt = DateTime.Now;

        string json = JsonConvert.SerializeObject(product,
                            Newtonsoft.Json.Formatting.Indented,
                            new JsonSerializerSettings {
                                NullValueHandling = NullValueHandling.Ignore
                            });
        Console.WriteLine(json);
    }
}

public class Product
{
    public string Name
    {
        get;
        set;
    }

    public DateTime CreatedAt
    {
        get;
        set;
    }
}
// Output:
//      {
//        "Name": "Apple",
//        "CreatedAt": "2016-06-26T23:46:57.3244307+00:00"
//      }

Go的encoding/json package、C#的Json.NET默认把日期类型序列化成ISO 8601标准的格式,JavaScript默认把Date序列化从1970年1月1日0点0分0秒的毫秒数。但JavaScript的dateObj.toISOString()能够将日期类型转成ISO格式的字符串,Date.parse(dateString)方法能够将ISO格式的日期字符串转成日期。

个人认为ISO格式的日期字符串可读性更好,但序列化和反序列化时的性能应该比整数更低。这一点从Go语言中time.Time的定义看出来。

type Time struct {
    // sec gives the number of seconds elapsed since
    // January 1, year 1 00:00:00 UTC.
    sec int64

    // nsec specifies a non-negative nanosecond
    // offset within the second named by Seconds.
    // It must be in the range [0, 999999999].
    nsec int32

    // loc specifies the Location that should be used to
    // determine the minute, hour, month, day, and year
    // that correspond to this Time.
    // Only the zero Time has a nil Location.
    // In that case it is interpreted to mean UTC.
    loc *Location
}

具体选择哪种形式在JSON中表示日期,有如下几点需要注意:

  • 选择标准格式。曾记得.NET Framework官方序列化JSON的方法中,会把日期转成如"\/Date(1343660352227+0530)\/"的专有格式,这样的专有格式对跨语言的访问特别不友好。
  • 如果你倾向性能,可以使用整数。如果你倾向可读性,可以使用ISO字符串。
  • 如果使用整数表示日期,而你的应用又是需要支持跨时区的,注意一定要是从1970-1-1 00:00:00 UTC开始计算的毫秒数,而不是当前时区的1970-1-1 00:00:00

参考

文章:

第三方类库:

工具:

  • JSON-to-Go: instantly converts JSON into a Go type definition