简谈 go 的数值类型
数值类型是一门编程语言中经常使用到的类型之一。go 语言原生支持整型、浮点型、复数类型。
整型
整型可分为平台无关整型和平台相关整型,区别在于,不同 CPU 架构或操作系统下面整型的长度是否是一致。
平台无关整型
| 类型 | 有无符号 | 长度(字节) | 取值范围 |
|---|---|---|---|
| int8 | 有 | 1 | [-128,127] |
| int16 | 有 | 2 | [-32768,32767] |
| int32 | 有 | 4 | [-2147483648,2147483647] |
| int64 | 有 | 8 | [- 9223372036854775808, 9223372036854775807] |
| uint8 | 无 | 1 | [0,255] |
| uint16 | 无 | 2 | [0,65535] |
| uint32 | 无 | 4 | [0,4294967295] |
| uint64 | 无 | 8 | [O, 18446744073709551615] |
类型有无符号的区别本质是最高二进制位(bit 位)是否被解释为符号位,这影响到有无符号整型的取值范围
例:
| 类型 | 最高位是否为符号位 | 二进制 | 十进制 |
|---|---|---|---|
| int8 | 是 | 10000001 | -128 |
| uint8 | 否 | 10000001 | 129 |
go 采用 2 的补码作为整型的比特位编码方法。go 的补码是通过将原码逐位取反,再加 1 的到。
例:
十进制 127 => 二进制 01111111 => 取反 10000000 => 加一 10000001
平台相关整型
| 类型 | 有无符号 | 长度(字节) |
|---|---|---|
| int | 有 | 32 位(4 字节) |
| uint | 无 | 32 位(4 字节) |
| uintptr | 无 | go 规范描述:“大到足以存储任意一个指针的值” |
因此编写有移植性要求的程序时,不能强依赖这些平台相关整型的长度。可以通过 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) |
|---|---|---|
| sign | exponent | maintissa |
浮点数的值 = (-1)^S - 1.M - 2^(E-offset) offset 为阶码偏移值
| 浮点类型 | 符号位(bit 位数) | 阶码(bit 位数) | 阶码偏移值 | 尾数(bit 位数) |
|---|---|---|---|---|
| 单精度(float32) | 1 | 8 | 127 | 23 |
| 双精度(float64) | 1 | 11 | 1023 | 52 |
对于单精度浮点数,阶码= 指数+偏移值,偏移值 = 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) |
|---|---|---|
| 0 | 10000110 | 00010111101(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,因此相等