从Go的1-2不等于-1聊起
从Go的1-2不等于-1聊起
ivansli1. 一道小学数学题引发的问题
首先从标题来看,你可能会认为有点瞧不起人,1-2等于几还用说?这分明就是小学的数学题嘛,最起码出一道初中数学题来讨论才对!
如果你也是这样想的,那么我只能说 talk is cheap,it is time show the code。
请看题:
1 | package main |
请问C的值是什么:
A. -1
B. 4294967295
C. 不知道
结论是:
如果选择A,那么本文可能会让你重新审视自己。
如果选择B,那么本文可能就不用再看了。
如果选择C,那么还等什么,接着向下看。
答案是:B
2. 抛出3个问题
针对上面代码的执行结果,抛出3个关键性问题:
- 常量a与变量c的数据类型是什么?
- 数值运算时类型是怎么转换的?
- 数值底层是怎么处理的?
2.1 常量a与变量c的数据类型
1 | package main |
执行结果:
1 | const a type is: int |
在go中,定义常量、变量时可以不用写类型,其实际数据类型由编译器根据其上下文来进行推导。
常量a的类型为int,是因为在定义常量a时,其右值为整形数值,所以被推导为默认数值类型int。
变量c是一个二元运算的结果,其数据类型是uint32,则需要遵循一些规则。
2.2 数值运算时类型转换规则
- 如果
两个操作数都为类型确定值
,则运算结果也是一个和这两个操作数相同的类型确定值。 - 如果
只有一个操作数是类型确定值
,则此运算的结果是一个和此类型确定操作数类型相同的类型。另一个类型不确定操作数的类型将被推导为(或隐式转换为)此类型确定操作数的类型。 - 如果
两个操作数均为类型不确定值
,则此运算的结果也是一个类型不确定的值。在运算中,两个操作数的类型将被假设为它们的默认类型中的一个(按照以下优先级:complex128 > float64 > rune > int
)。结果的默认类型同样为此假设类型。比如:如果一个类型不确定操作数的默认类型为int,另一个类型不确定的操作数的默认类型为rune,则前者的类型在运算中也被视为rune,运算结果为一个类型为rune的类型值。
c:=a-b
中由于变量b的类型是确定的(为uint32),根据规则2,则运算结果c的类型也为uint32。运算中常量a的类型虽然默认为int,也会被隐式转换为uint32(go是强类型语言,运算的数据类型必须一致)。
2.3 数值底层处理方式
在计算机系统中,数值一律使用补码
表示(存储)。
主要原因:使用补码,可以将符号位和其它位统一处理。同时,减法也可按加法来处理。另外,两个用补码表示的数相加时,如果最高位(符号位)有进位,则进位被舍弃。
1 | 原码->补码遵循以下规则: |
- 正数的补码:与原码相同。
例如,+9 的原码是00001001,补码也是00001001。
- 负数的补码:符号位为1,其余位为该数绝对值的原码按位取反,然后加1。
例如,-7的补码:因为是负数,则符号位为1,整个为10000111。除符号位,其余7位为-7的绝对值+7的原码0000111,按位取反为1111000,再加1,所以-7的补码是11111001。
1 | 补码->原码遵循以下规则: |
- 如果补码的符号位为0,表示是一个正数,所以补码就是该数的原码。
- 如果补码的符号位为1,表示是一个负数。求原码的操作是:符号位为1,其余各位取反,然后加1。
例如,已知一个补码为11111001,则原码是10000111(-7)。因为符号位为1,表示是一个负数,所以符号位不变为1。其余7位1111001取反后为0000110,再加1,所以是10000111。
3. 1-2为什么不等于-1
通过上面的若干规则知道,a-b(1-2) != -1
的原因主要有2点:
- 由于
c:=a-b
中只有变量b的类型是确定的uint32,所以能确定运算结果c的类型也必定是uint32。另一个不确定类型的操作数a的类型会被推导或者转换为uint32。常量a由于没有写明类型,虽然其默认类型为int,但是由于其参与的运算中变量b是类型确定的uint32,所以会被转换为uint32参与运算。 - a-b 可以视为 a+(-b),a的补码表示为00000000000000000000000000000001,-b的补码表示为11111111111111111111111111111110。a+(-b)的运算结果c的补码表示为11111111111111111111111111111111。由于结果类型为uint32(无符号型),则c的补码的符号位1并不表示负数。所以,转换为10进制之后,得出c的结果为4294967295。
4. 总结
经过一顿操作,才发现原来这道小学数学题也不是那么容易做出来的。由于,其牵扯的底层原理、类型转换细节容易被忽略,可能在开发中就会出现自我感觉良好,一上线却造成了莫名其妙的bug。
想说的是,通过这段代码让我们认识到:学好底层原理,打好基础的重要性。