简谈 go 的数值类型

346 阅读6分钟

简谈 go 的数值类型

数值类型是一门编程语言中经常使用到的类型之一。go 语言原生支持整型、浮点型、复数类型。

整型

整型可分为平台无关整型和平台相关整型,区别在于,不同 CPU 架构或操作系统下面整型的长度是否是一致。

平台无关整型

类型有无符号长度(字节)取值范围
int81[-128,127]
int162[-32768,32767]
int324[-2147483648,2147483647]
int648[- 9223372036854775808, 9223372036854775807]
uint81[0,255]
uint162[0,65535]
uint324[0,4294967295]
uint648[O, 18446744073709551615]

类型有无符号的区别本质是最高二进制位(bit 位)是否被解释为符号位,这影响到有无符号整型的取值范围

例:

类型最高位是否为符号位二进制十进制
int810000001-128
uint810000001129

go 采用 2 的补码作为整型的比特位编码方法。go 的补码是通过将原码逐位取反,再加 1 的到。

例:

十进制 127 => 二进制 01111111 => 取反 10000000 => 加一 10000001

平台相关整型

类型有无符号长度(字节)
int32 位(4 字节)
uint32 位(4 字节)
uintptrgo 规范描述:“大到足以存储任意一个指针的值”

因此编写有移植性要求的程序时,不能强依赖这些平台相关整型的长度。可以通过 unsafe 包的 SizeOf 函数获取这三个类型宰目标运行平台上的长度。

整型溢出问题

无论哪种整型类型都有其取值范围,当运算结果超出这个整型的取值范围,就被称为发生==整型溢出==问题。 由于整型无法表示它溢出后的那个“结果”,所以出现溢出情况后,对应的整型变量的值依然会落到它的取值范围内,只是结果值与我们的预期不符,导致程序逻辑出错。

例:

var s int8 = 127
s += 1 // 预期128,实际结果-128

var u uint8 = 1
u -= 2 // 预期-1,实际结果255

字面值与格式化

a := 53        // 十进制
b := 0700      // 八进制,以"0"为前缀
c1 := 0xaabbcc // 十六进制,以"0x"为前缀
c2 := 0Xddeeff // 十六进制,以"0X"为前缀

d1 := 0b10000001 // 二进制,以"0b"为前缀
d2 := 0B10000001 // 二进制,以"0B"为前缀
e1 := 0o700      // 八进制,以"0o"为前缀
e2 := 0O700      // 八进制,以"0O"为前缀

// go 1.13版本后出现的数字分隔符
f := 5_3_7   // 十进制: 537
g := 0b_1000_0111  // 二进制位表示为10000111
h1 := 0_700  // 八进制: 0700
h2 := 0o_700 // 八进制: 0700
i := 0x_5c_6d // 十六进制:0x5c6d


// 格式化
var a int8 = 59
fmt.Printf("%b\n", a) //输出二进制:111011
fmt.Printf("%d\n", a) //输出十进制:59
fmt.Printf("%o\n", a) //输出八进制:73
fmt.Printf("%O\n", a) //输出八进制(带0o前缀):0o73
fmt.Printf("%x\n", a) //输出十六进制(小写):3b
fmt.Printf("%X\n", a) //输出十六进制(大写):3B

浮点型

IEEE 754 标准规定了四种表示浮点数值的方式:单精度(32 位)、双精度(64 位)、扩展单精度(43 比特以上)与扩展双精度(79 比特以上,通常以 80 位实现)。

Go 语言提供了 float32 与 float64 两种浮点类型,它们分别对应的就是 IEEE 754 中的单精度与双精度浮点数值类型。Go 语言中没有提供 float 类型。换句话说,Go 提供的浮点类型都是平台无关的。

float32 和 float64,变量的默认值都是 0.0,区别在于占用的内存空间大小不一样,可以表示的浮点数的范围与精度也不同。

点数在内存中的二进制表示(Bit Representation)要比整型复杂得多,IEEE 754 规范给出了在内存中存储和表示一个浮点数的标准形式如下:

符号位(S)阶码/指数(E)尾数(M)
signexponentmaintissa

浮点数的值 = (-1)^S - 1.M - 2^(E-offset) offset 为阶码偏移值

浮点类型符号位(bit 位数)阶码(bit 位数)阶码偏移值尾数(bit 位数)
单精度(float32)1812723
双精度(float64)111102352

对于单精度浮点数,阶码= 指数+偏移值,偏移值 = 2^(e-1)-1,其中 e 为阶码部分的 bit 位数,也就是 8。所以单精度浮点数的阶码偏移值为 2^(8-1)-1 = 127。双精度浮点数同理。

如何将一个十进制的浮点值转化为 IEEE754 规定的单精度二进制表示

例: 十进制数 139.8125 步骤 1: 将整数位与小数位分别转化为二进制,得到 10001011.1101 步骤 2: 移动小数点,至整数部分仅有一个 1,即 10001011.1101 => 1.00010111101, 移动了 7 位,所以==指数==为 7,尾数为 00010111101。 步骤 3: 计算阶码,指数 7 + 偏移值 127 = 阶码(十进制) 134 => 阶码(二进制) 10000110 步骤 4: 按 IEEE754 规范 排列 符号位、阶码、尾数,尾数不足 23 位则在后面补 0,如下:

符号位(S)阶码/指数(E)尾数(M)
01000011000010111101(000000000000)

所以 最终浮点数 139.8125d 的二进制表示就为 0b_0_10000110_00010111101_000000000000。

最后,再通过 Go 代码输出浮点数 139.8125d 的二进制表示,做一下比对

func main() {
    var f float32 = 139.8125
    bits := math.Float32bits(f)
    fmt.Printf("%b\n", bits)
}

// output:
// 1000011000010111101000000000000 一致

在这段代码中,我们通过标准库的 math 包,将 float32 转换为整型。在这种转换过程中,float32 的内存表示是不会被改变的。

因此,阶码和尾数的长度决定了浮点类型可以表示的浮点数范围与精度。因为双精度浮点类型(float64)阶码与尾数使用的比特位数更多,它可以表示的精度要远超单精度浮点类型,所以在日常开发中,使用双精度浮点类型(float64)的情况更多,这也是 Go 语言中浮点常量或字面值的默认类型。

问题

两个 float32 为什么会相等呢?

var f1 float32 = 16777216.0
var f2 float32 = 16777217.0
fmt.Println(f1 == f2) // true

答:两个数转成二进制数由于尾数都超出 23 位,截取前 23 位后是一直的,所以最后结果都是 1266679808,因此相等