乐趣区

关于golang:go-int64传递到前端导致溢出问题排查

简介

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

集体推广

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

退出移动版