简介

​ 开周会的时候一位同事分享了一个踩坑经验,说在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) {
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的数据表示有精度问题,单独看他两是没有什么问题的,就像好好的两个人,在一起却不合适一样。其次还要对计算机底层的知识比较了解。这也侧面反映了,咱还是得多多学习。

参考资料

个人推广

​ 下面是笔者的公众号,希望兄弟们可以多多关注,感谢您的支持啦~

go int64传到前端导致溢出问题排查的更多相关文章

  1. 后端Long类型传到前端精度丢失的正确解决方式

    原因:前端js对Long类型支持的精度不够,导致后端使用的Long传到前端丢失精度,比如现在分布式id生成算法"雪花算法"在使用中就会出现问题. 解决方式: 1.后端的Long类型 ...

  2. xss-跨站脚本攻击-后台传给前端的html标签安全显示

    作用 后台拼接的html字符串传到前端,默认是不安全的,需要告诉前端这个字符串是安全的,可以正常显示html标签. 知识点 1.定义 2 3 <script>      获取session ...

  3. 在C#后端处理一些结果然传给前端Javascript或是jQuery

    在C#后端处理一些结果然传给前端Javascript或是jQuery,以前Insus.NET有做过一个例子<把CS值传给JS使用 >http://www.cnblogs.com/insus ...

  4. 关于javaBean中boolean类型变量的set和get注入后传到前端JS中的问题

    set和get方法如下: public boolean isLine() {        return isLine;    } public void setLine(boolean isLine ...

  5. django--如何将数据结果集序列化传给前端页面展示

    示例为一对多的表关系,学生为多,老师为一,设置外键字段可以为空,也就是说关联的老师被删除该学生依然存在,只是相应字段留空 class Teacher(models.Model): name = mod ...

  6. C#执行Sql 时,出现“算术运算导致溢出”问题,如何解决?

    昨天在C#执行oracle的sql语句时,总是报错,原先在pl/sql 执行sql语句是可以的,在C#执行就报“算术运算导致溢出”问题 SQL语句 select A.SKU_ID 商品标识,A.COL ...

  7. Django 模板语言从后端传到前端

    如果我们在后端有数据动态提取到前端的时候 就需要模板语言加以渲染后再将渲染好的HTML文件传入前端 我们的views.py里的index函数里有个s变量是个列表,将数据以大括号的形式传入{" ...

  8. sql语句执行时算术运算导致溢出。

    执行sql语句时报错: 用户代码未处理 System.OverflowException HResult=-2146233066 Message=算术运算导致溢出. 文章:https://bbs.cs ...

  9. 后端传给前端Long类型数据,导致精度丢失

    1.前几天遇到了一个问题,后端向前端传递一个Long类型的数据,前端拿到的值不对. 2.当Long类型的数据大于17位时候,就会出现精度丢失的情况. 3.解决办法 我们采用的解决方案是将后端的Long ...

随机推荐

  1. 深入理解Kafka核心设计及原理(五):消息存储

    转载请注明出处:https://www.cnblogs.com/zjdxr-up/p/16127749.html 目录: 5.1文件目录布局 5.2消息压缩 5.3日志索引 5.4日志文件及索引文件分 ...

  2. 2021.11.10 fail树

    2021.11.10 fail树 https://blog.csdn.net/niiick/article/details/87947160 1. AC自动机与fail树的神奇关系 1.1 AC自动机 ...

  3. python基础练习题(题目 取一个整数a从右端开始的4〜7位。)

    day36 --------------------------------------------------------------- 实例054:位取反.位移动 题目 取一个整数a从右端开始的4 ...

  4. 安装黑苹果 、 Mac OS虚拟机

    Mac OS 虚拟机 所需文件地址 unlocker 为VMware 新增Apple Mac OS X操作系统 Install_macOS_Monterey_12.0.1_21A559.iso 提取码 ...

  5. lab_1 清华大学ucore bootload启动ucore os(预备基础知识+实验过程)

    实验1 :bootload启动ucore os 1.0实验内容: lab1中包含一个bootloader和一个OS.这个bootloader可以切换到X86保护模式,能够读磁盘并加载ELF执行文件格式 ...

  6. 分享一下 Idea 的 scope 功能

    分享一下 Idea 的 scope 功能 事情的起因是我在使用 idea 的call hierarchy功能时,觉得它没有像find usage那样有排除功能,并且如果点击了展开全部,当代码中使用了某 ...

  7. 数据库界的Swagger:一键生成数据库文档!

    对于开发的API文档,我们可以通过Swagger等工具来自动生成了.但是对于数据库表结构的文档呢,在实际开发中在开发前我们一般会先设计好表结构,大家讨论一下, 这个时候就很需要有个数据库表结构的文档, ...

  8. Java泛型类型擦除问题

    以前就了解过Java泛型的实现是不完整的,最近在做一些代码重构的时候遇到一些Java泛型类型擦除的问题,简单的来说,Java泛型中所指定的类型在编译时会将其去除,因此List 和 List 在编译成字 ...

  9. 关于Linux添加字体

    安装字体命令 yum -y install fontconfig 查看已经安装的字体 fc-list # 查看已经已经安装的中文字体 fc-list :lang=zh 在字体目录下创建新的目录或者使用 ...

  10. Blazor和Vue对比学习(基础1.9):表单输入绑定和验证,VeeValidate和EditFrom

    这是基础部分的最后一章,内容比较简单,算是为基础部分来个HappyEnding.我们分三个部分来学习: 表单输入绑定 Vue的表单验证:VeeValidate Blazor的表单验证:EditForm ...