Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

json.Unmarshal 奇怪的坑 #50

Open
jinhailang opened this issue May 15, 2019 · 1 comment
Open

json.Unmarshal 奇怪的坑 #50

jinhailang opened this issue May 15, 2019 · 1 comment

Comments

@jinhailang
Copy link
Owner

jinhailang commented May 15, 2019

encoding/json 是 Go 代码经常使用的包,但是,可能很多人都会忽略下面这段说明:

To unmarshal JSON into an interface value, Unmarshal stores one of these in the interface value:

bool, for JSON booleans
float64, for JSON numbers
string, for JSON strings
[]interface{}, for JSON arrays
map[string]interface{}, for JSON objects
nil for JSON null

当 json 解码到 interface 类型的变量值时,会将 JSON numbers(实质是 string 类型,表示整数或浮点数数字字符串)都当作类型 float64 存储。

试想以下代码输出?

package main

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

func main() {
	s := `{"name":"test","it":1021,"timestamp":1557822591000,"mmp":{"a":"ax","b":999999}}`
	type st struct{
		Name string
		Timestamp interface{}
		Mmp interface{}
		It interface{}
	}
	var tmp st
	
	err := json.Unmarshal([]byte(s),&tmp)
	fmt.Printf("tmp: %+v, err: %v\r\n",tmp, err)
		
	fmt.Printf("%v, %v\r\n",reflect.TypeOf(tmp.Timestamp), tmp.Timestamp)
}


tmp: {Name:test Timestamp:1.557822591e+12 Mmp:map[a:ax b:999999] It:1021}, err: <nil>
float64, 1.557822591e+12

完成 Json 解码后,Timestamp 类型为 float64。这显然是无法让人接受的,就这里来说,时间戳应该是 int64 才对。目前,有两个解决办法:

  1. 显示声明类型

避免使用 interface,而是直接静态类型指定,在大多数情况下,Json 字符串结构都是已知的,静态的。
上面的场景,就可以将时间戳属性定义为 Timestamp int64

  1. 使用函数 UseNumber()

func (*Decoder) UseNumber() 使解码器将数字作为 json.Number 类型,
而不 float64 解码到 interface 变量。

    ...

    ds := json.NewDecoder(strings.NewReader(s))
	ds.UseNumber()
	err := ds.Decode(&tmp)
	
	fmt.Printf("tmp: %+v, err: %v\r\n",tmp, err)
		
	//rf, _ := strconv.ParseFloat("123.90",64)
	fmt.Printf("%v, %v\r\n",reflect.TypeOf(tmp.Timestamp), tmp.Timestamp)


tmp: {Name:test Timestamp:1557822591000 Mmp:map[a:ax b:999999] It:1021}, err: <nil>
json.Number, 1557822591000

可以看到,json.Number 其实就是字符串类型:

type Number string

因此,这里其实就是保留原始字符串,延迟解析。在需要的时候,使用提供的函数 Float64(), Int64() 等转化成对应的类型,其实,这些函数的实现就是使用 strconv 包将字符串转化成整型或浮点型。

但是,这里引入了一个新的类型 json.Number,会侵入到别的无关的代码中,也就是说,可能会导致,在其它模块,不得不在类型判断时,加入 json.Number case。这种耦合是比较让人难受的。

遗憾的是,目前看来,只有这两种方式了,虽然都不够优雅。

这是个很奇怪的问题,因为技术上来说,将数字字符串分别解析为整型或浮点型并不难实现,Go 编译器就很好的实现了(想想 x:=100x:=100.0 的区别);
而且,如果 Json 数字的含义是整型,默认却解析成 float64 就会有精度丢失的问题,因为 int64 比 float64 表示的范围更大。

Go issues 找了下,也并没有看到合理的解释,难道只是为了实现方便,偷了个懒?真是个奇怪的坑!

相关 issues:

补充

感谢Go 论坛网友 @H12 的指正:

float64 的表示范围显然远大于int64/uint64 (ref. math. MaxFloat64, math. MaxInt64),只是在表示整数时有可能有精度损失。
JSON (json.org)并没有规定number的精度和大小范围,所以即使用uint64或int64,在解析整数时仍然存在溢出的可能。这时如果用float64来解析,因为表示范围大于int64,溢出的可能性更小,所以更安全(精度损失总比溢出强)。如果追求完全不溢出,可以用 type Number string。

Go 中的 float64 其实等同于 double 类型:

1bit(符号位) 11bits(指数位) 52bits(尾数位)

范围是 -2^1024 ~ +2^1024,也即 -1.79E+308 ~ +1.79E+308。精度是由尾数的位数来决定的。浮点数在内存中是按科学计数法来存储的,其整数部分始终是一个隐含着的“1”,由于它是不变的,故不能对精度造成影响:2^52 = 4503599627370496,一共16位,因此,double的精度为15~16位。而 int64 等同于 long, 占8个字节,表示范围: -9223372036854775808 ~ 9223372036854775807

因此,出现这个坑的原因,是设计上的取舍,为了保证 Json 数字解析安全(不溢出),只能牺牲精度。

@michaelkidpro
Copy link

可以把时间戳 都转成string,再json序列化。 json转map后也时间戳也是string

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants