简介
开周会的时候一位共事分享了一个踩坑教训,说在 go 外面还好好的 int64 类型,到前端就变得奇奇怪怪了,和原来不一样了。正好我对前端 javascript 有一点点理解,而后连夜写了点代码摸索了一下这个问题。这个问题的实质是 javascript number 类型能示意的数据范畴不能残缺包含 go 中 int64 的范畴导致的。上面看笔者娓娓道来。
踩坑剖析
话不多说,咱们应用以下代码构建一下 go http 后端试验场景。上面代码提供了 go 原生的 http api 和 http 框架 gin 两种形式启动 http 服务。读者们能够运行这两种形式之中的任意一种去将服务跑起来。
type Data struct {Id int64 `json:"id"`}
// 构建试验数据。var int64Data = []Data{
{Id: 1,},
{Id: 2,},
{Id: 3,},
{Id: 1 << 53,},
{Id: 1<<53 + 1,},
{Id: 1<<53 + 2,},
{Id: 1<<53 + 3,},
}
// go 原生 http 服务
func TestHttpJson(t *testing.T) {var httpJsonTestHandler = func(w http.ResponseWriter, r *http.Request) {log.Println("com here")
r.Header.Set("Access-Control-Allow-Origin", "*")
w.Header().Set("content-type", "text/json")
if resByte, jsonErr := json.Marshal(int64Data); jsonErr != nil {w.Write([]byte(jsonErr.Error()))
log.Fatal(jsonErr)
} else {w.Write(resByte)
}
return
}
http.HandleFunc("/json_test", httpJsonTestHandler)
err := http.ListenAndServe(":8888", nil)
if err != nil {log.Fatal("ListenAndServe:", err)
}
select {}}
// gin http 服务
func TestJsonNumberWithGin(t *testing.T) {r := gin.Default()
r.GET("json_test", func(c *gin.Context) {c.Request.Header.Set("Access-Control-Allow-Origin", "*")
c.JSON(http.StatusOK, int64Data)
})
r.Run(":8888")
}
运行起来服务之后,咱们应用 postman 拜访一下这个接口。这看起来没有什么问题啊,一切正常的样子。然而这只是表象。因为咱们返回的是字符数组,postman 读的也是字符数组,所以相当于 postman 拿到字符数组之后拼接成字符串展现给咱们,这样子的话无论你后端的 http 接口返回的是什么,postman 都能给你做一摸一样的展现。同理,在浏览器上拜访这个接口意识这样的,数据看不出什么同样。
这里其实是障眼法,当咱们看到在 postman 上展现没有问题,就会天经地义的感觉在理论的前端 javasrcipt 运行环境中没有问题,从而漠视这个问题。然而事实往往和现实是有差距的,上面咱们看看在 javascript 中理论的运行成果吧。这里笔者用的是 node.js 拜访 http 接口的形式。如果机器本地没有 node.js 倡议装置一下,如果你和笔者一样用的是 mac,执行上面指令即可装置:
brew install node
如果是 windows, 去 node 官网(https://nodejs.org/en/download/)下载之后傻瓜式装置即可。
在 javascript 中拜访 http 接口,笔者这里用的是 axios,也算是前端中比拟罕用的 http 库了,能够通过运行上面脚本装置:
npm install axios
上面是 javascript 拜访 http 接口代码 jsonNumberTest.js:
const axios = require('axios');
axios.get('http://127.0.0.1:8888/json_test').then(
res => {const { data} = res
console.info(data)
}
).catch(err => {console.log(err.message)
})
通过运行上面脚本执行这个 javascript 程序:
node jsonNumberTest.js
让咱们来看看后果吧。
这里咱们能够看到他的不同之处了,对于这个数组的前三个数,1,2,3 没什么同样,然而前面这些数字显著就不一样了。咱们 http 接口中返回的四个数字是 1 <<53, 1 << 53+1, 1<< 53 + 2, 1 << 53 + 3,然而咱们看这个在 nodejs 中的打印进去的显著就不具备连续性。问题来了,是什么货色造成的呢。
事件的假相是 javascript 中的数字只有 number 这个数据类型是浮点数,不像 go 中具备那么丰盛的根底数据类型,也就是说在 go 中的 int, int32, int64, float32….,这些与数字相干的数据类型到了 js 这里只有一个 number 类型和他对应,如果 go 中的数字超出 javascript number 能示意的范畴,就会呈现下面的景象。上面咱们来看下 javascript 中的 number 类型,其实也就是计算机中的浮点数示意。
计算机中的浮点数示意
咱们晓得,在事实世界中,两个小数之间存在着有数多的小数,然而计算机能示意的数是无限的,所以在计算机中浮点数会存在肯定的精度缺失,也就是说又一些小数只能迫近,却不能够准确的示意。
以后大多数编程语言采纳的都是国际标准 IEEE754 实现浮点数的存储。在 IEEE754 中,每一个二进制表达式的值能够通过上面公式计算取得:
$$
V = (-1)^s * M * 2^E
$$
公式中的字母含意与在二进制表达式中的地位如下图所示。
- 对于单精度浮点数,符号位占 1 位,指数位占 8 位,无效数位占 23 位
- 对于双精度浮点数,符号位占 1 位,指数位占 11 位,无效数位占 52 位
IEEE754 规定在规范化示意的时候,M 要写成 1.XXXXXXX 的模式,其中 XXXXXXX 是小数局部。这里的 1 不须要存储,理论计算的时候把他加上就好。
另外在迷信计数法中 E 是能够呈现正数的,所以为了 E 的负数局部和正数局部的散布是平均的,在计算的时候指数位的值要减去指数位示意范畴的两头值。什么意思呢?也就是说如果是在单精度的浮点数示意,指数位占 8 位,那么他的取值范畴就是 [0, 255], 那么要减去的两头数是 127,那么如果理论计算的时候指数地位的值位 10(十进制),那么理论在指数位存储的应该是 10 + 127 = 137。同理双精度的取值范畴是 [0,2047],那么两头数就是 1023.
上面咱们举一个例子,咱们看下单精度浮点数 6.625 是怎么示意的。首先咱们把这个数变成二进制。
$$
110.101
$$
而后咱们进行规范化示意,也就是说把他变成小数点前只有一个数字的模式, 并且因为无效位是 23 位,所以不够的数要补 0.
$$
1.1010 1000 0000 0000 0000 000 * 2 ^2
$$
所以这个数的符号位是 0,有效数字 M 为 1010 1000 0000 0000 0000 000,指数 E 为 2 + 127 = 129 = 100000001,那么这个数的存储格局为:
$$
0 10000001 1010 1000 0000 0000 0000 000
$$
双精度浮点数和单精度浮点数的存储是一样的原理,这里就不多赘述了。
问题定位及修改办法
通过下面对于浮点数示意的讲述,咱们很容易晓得双精度浮点数能示意的最大的整数值为:
$$
(-1)^0*\sum_{n=1}^{52}{2^n}
$$
也就是有效数字为全是 1,指数位为 52(计算的时候消去小数点,只保留整数)。这个值就是 2 << 53 – 1. 所以在 go int64 类型的数字通过 http 传给前端的时候大于 2 << 53 – 1 的数都会因为溢出而没法失常示意。
解决这个问题比拟好的办法就是传 String 给前端,不再传 int64 这种危险的数字了,如果这个数字是对应的是数据库中一条记录的 id,前面前端实现操作之后要回传这个 id 给后端进行 update 操作,就会因为传的 id 与原来不一样导致更新别的记录,使得数据呈现问题。
总结
发现与定位这个问题其实并不容易,一方面是其实问题呈现在 go 的数据传给 js 的时候,js 的数据表示有精度问题,独自看他两是没有什么问题的,就像好好的两个人,在一起却不适合一样。其次还要对计算机底层的常识比拟理解。这也侧面反映了,咱还是得多多学习。
参考资料
- 为什么 0.1+0.2 不等于 0.3: https://draveness.me/whys-the…
- CSAPP 第二章:信息的示意和解决
集体推广
上面是笔者的公众号 –“陪计算机走过漫长岁月”,心愿兄弟们能够多多关注,感谢您的反对啦~