简介

开周会的时候一位共事分享了一个踩坑教训,说在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第二章:信息的示意和解决

集体推广

上面是笔者的公众号--“陪计算机走过漫长岁月”,心愿兄弟们能够多多关注,感谢您的反对啦~