从Go的1-2不等于-1聊起

1. 一道小学数学题引发的问题

首先从标题来看,你可能会认为有点瞧不起人,1-2等于几还用说?这分明就是小学的数学题嘛,最起码出一道初中数学题来讨论才对!

如果你也是这样想的,那么我只能说 talk is cheap,it is time show the code。

请看题:

1
2
3
4
5
6
7
8
9
10
11
12
package main

import"fmt"

const a = 1

func main() {
var b uint32 = 2
c := a - b

fmt.Println(c)
}

请问C的值是什么:
A. -1
B. 4294967295
C. 不知道

结论是:
如果选择A,那么本文可能会让你重新审视自己。
如果选择B,那么本文可能就不用再看了。
如果选择C,那么还等什么,接着向下看。

答案是:B

2. 抛出3个问题

针对上面代码的执行结果,抛出3个关键性问题:

  1. 常量a与变量c的数据类型是什么?
  2. 数值运算时类型是怎么转换的?
  3. 数值底层是怎么处理的?

2.1 常量a与变量c的数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import"fmt"

const a =1

func main() {
fmt.Printf("const a type is: %T\n", a)

var b uint32 = 2
c := a - b

fmt.Printf("var c type is: %T\n", c)
}

执行结果:

1
2
const a type is: int
var c type is: uint32

在go中,定义常量、变量时可以不用写类型,其实际数据类型由编译器根据其上下文来进行推导。

常量a的类型为int,是因为在定义常量a时,其右值为整形数值,所以被推导为默认数值类型int。

变量c是一个二元运算的结果,其数据类型是uint32,则需要遵循一些规则。

2.2 数值运算时类型转换规则

  1. 如果两个操作数都为类型确定值,则运算结果也是一个和这两个操作数相同的类型确定值。
  2. 如果只有一个操作数是类型确定值,则此运算的结果是一个和此类型确定操作数类型相同的类型。另一个类型不确定操作数的类型将被推导为(或隐式转换为)此类型确定操作数的类型。
  3. 如果两个操作数均为类型不确定值,则此运算的结果也是一个类型不确定的值。在运算中,两个操作数的类型将被假设为它们的默认类型中的一个(按照以下优先级:complex128 > float64 > rune > int)。结果的默认类型同样为此假设类型。比如:如果一个类型不确定操作数的默认类型为int,另一个类型不确定的操作数的默认类型为rune,则前者的类型在运算中也被视为rune,运算结果为一个类型为rune的类型值。

c:=a-b中由于变量b的类型是确定的(为uint32),根据规则2,则运算结果c的类型也为uint32。运算中常量a的类型虽然默认为int,也会被隐式转换为uint32(go是强类型语言,运算的数据类型必须一致)。

2.3 数值底层处理方式

在计算机系统中,数值一律使用补码表示(存储)。

主要原因:使用补码,可以将符号位和其它位统一处理。同时,减法也可按加法来处理。另外,两个用补码表示的数相加时,如果最高位(符号位)有进位,则进位被舍弃。

1
原码->补码遵循以下规则:
  1. 正数的补码:与原码相同。

例如,+9 的原码是00001001,补码也是00001001。

  1. 负数的补码:符号位为1,其余位为该数绝对值的原码按位取反,然后加1。

例如,-7的补码:因为是负数,则符号位为1,整个为10000111。除符号位,其余7位为-7的绝对值+7的原码0000111,按位取反为1111000,再加1,所以-7的补码是11111001。

1
补码->原码遵循以下规则:
  1. 如果补码的符号位为0,表示是一个正数,所以补码就是该数的原码。
  2. 如果补码的符号位为1,表示是一个负数。求原码的操作是:符号位为1,其余各位取反,然后加1。

例如,已知一个补码为11111001,则原码是10000111(-7)。因为符号位为1,表示是一个负数,所以符号位不变为1。其余7位1111001取反后为0000110,再加1,所以是10000111。

3. 1-2为什么不等于-1

通过上面的若干规则知道,a-b(1-2) != -1的原因主要有2点:

  1. 由于c:=a-b中只有变量b的类型是确定的uint32,所以能确定运算结果c的类型也必定是uint32。另一个不确定类型的操作数a的类型会被推导或者转换为uint32。常量a由于没有写明类型,虽然其默认类型为int,但是由于其参与的运算中变量b是类型确定的uint32,所以会被转换为uint32参与运算。
  2. a-b 可以视为 a+(-b),a的补码表示为00000000000000000000000000000001,-b的补码表示为11111111111111111111111111111110。a+(-b)的运算结果c的补码表示为11111111111111111111111111111111。由于结果类型为uint32(无符号型),则c的补码的符号位1并不表示负数。所以,转换为10进制之后,得出c的结果为4294967295。

4. 总结

经过一顿操作,才发现原来这道小学数学题也不是那么容易做出来的。由于,其牵扯的底层原理、类型转换细节容易被忽略,可能在开发中就会出现自我感觉良好,一上线却造成了莫名其妙的bug。

想说的是,通过这段代码让我们认识到:学好底层原理,打好基础的重要性。