annidy / notes

0 stars 0 forks source link

json int64 pitfall #287

Open annidy opened 1 month ago

annidy commented 1 month ago

JavaScript在解析Json时,对数值类型默认时Number类型。Number是一个双精度浮点数,能够安全表示的最大整数是Number.MAX_SAFE_INTEGER,其值为9007199254740991(即2^53 - 1)。这是因为在双精度浮点数中,指数部分有11位,尾数部分有52位(加上一个隐含的位),所以能够精确表示的整数位数受限于这52位尾数。

image

int64类型能表示最大为2^63 - 1 (9223372036854775807),中间差距还是很大的。在64位系统上,go语言int类型其实是int64,所以很多系统倾向于将int类型转为string给到前端,前端也会把整数转成字符串给go。

go的json是强类型,在解析的时候会报错

type User struct {
    Age int `json:"age"`
}

func main() {
    var u User
    if e := json.Unmarshal([]byte(`{"age": "9223372036854775807"}`), &u); e != nil {
       // panic: json: cannot unmarshal string into Go struct field User.age of type int
        panic(e) 
    }
    fmt.Printf("%+v\n", u)
}

如果不考虑兼容,最简单的改法是在tag中指定类型

type User struct {
    Age int `json:"age,string"`
}

但是这样就不支持旧的数字输入了,同样会返回类型错误。

方案1:使用json.Number

type User struct {
    Age json.Number `json:"age"`
}

func main() {
    var u User
    if e := json.Unmarshal([]byte(`{"age": "9223372036854775807"}`), &u); e != nil {
        panic(e)
    }
    fmt.Printf("%+v\n", u) // {Age:9223372036854775807}
    if e := json.Unmarshal([]byte(`{"age": 9223372036854775807}`), &u); e != nil {
        panic(e)
    }
    fmt.Printf("%+v\n,", u) // {Age:9223372036854775807}
    i, _ := u.Age.Int64()
    f, _ := u.Age.Float64()
    fmt.Println(i, f) // 9223372036854775807 9.223372036854776e+18 - float64溢出

    var u2 User
    u2.Age = json.Number("9223372036854775807")
    if b, e := json.Marshal(u2); e != nil {
        panic(e)
    } else {
        fmt.Println(string(b)) // {"age":9223372036854775807} -- js溢出
    }
}

这种方案是标准库内置方案,它可以像js一样,同时接受string和int类型,但在读取时需要额外的方法获取int64数值;在序列化时,输出的也是Number类型,可能导致js溢出

方案2:自定义Unmarshal

go允许为自定义类型实现Unmarshal方法,因此可以实现一个自定义的类型

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

type StringInt int

func (st *StringInt) UnmarshalJSON(b []byte) error {
    var item interface{}
    if err := json.Unmarshal(b, &item); err != nil {
        return err
    }
    switch v := item.(type) {
    case float64:
        *st = StringInt(int(v))
    case string:
        i, err := strconv.Atoi(v)
        if err != nil {
            return err

        }
        *st = StringInt(i)

    }
    return nil
}

type User struct {
    Age StringInt `json:"age"`
}

func main() {
    var u User
    if e := json.Unmarshal([]byte(`{"age": "9223372036854775807"}`), &u); e != nil {
        panic(e)
    }
    fmt.Printf("%+v\n", u) // {Age:9223372036854775807}
    if e := json.Unmarshal([]byte(`{"age": 9223372036854775807}`), &u); e != nil {
        panic(e)
    }
    fmt.Printf("%+v\n", u) // {Age:-9223372036854775808} -- 溢出
    if e := json.Unmarshal([]byte(`{"age": 9007199254740992}`), &u); e != nil {
        panic(e)
    }
    fmt.Printf("%+v\n", u) // {Age:9007199254740992}
}

⚠️ Go对json中的数字也是当做float64(同样是双精度)来处理的,超过9007199254740992也会出现溢出。 这个错误是很常见的,当json.Unmarshal类型为interface{}的对象时,都会优先使用float64。

一种解决方案是不使用interface{},但是需要2次Unmarshal操作

func (st *StringInt) UnmarshalJSON(b []byte) error {
    var item int
    var item_str string
    if err := json.Unmarshal(b, &item); err != nil {
        if err := json.Unmarshal(b, &item_str); err != nil {
            return err
        }
        i, err := strconv.Atoi(item_str)
        if err != nil {
            return err
        }
        *st = StringInt(i)
    } else {
        *st = StringInt(item)
    }
    return nil
}