go基础03-基本数据类型:Go原生支持的数值类型有哪些?

224 阅读7分钟

被广泛使用的整型

我们先来看平台无关整型,它们在任何 CPU 架构或任何操作系统下面,长度都是固定不变的。我在下面这张表中总结了 Go 提供的平台无关整型:

image.png 你可以看到,这些平台无关的整型也可以分成两类:有符号整型(int8int64)和无符号整型(uint8uint64)。两者的本质差别在于最高二进制位(bit 位)是否被解释为符号位,这点会影响到无符号整型与有符号整型的取值范围。

我们以下图中的这个 8 比特(一个字节)的整型值为例,当它被解释为无符号整型 uint8 时,和它被解释为有符号整型 int8 时表示的值是不同的:

image.png Go 采用 2 的补码(Two’s Complement)作为整型的比特位编码方法。因此,我们不能简单地将最高比特位看成负号,把其余比特位表示的值看成负号后面的数值。Go 的补码是通过原码逐位取反后再加 1 得到的,比如,我们以 -127 这个值为例,它的补码转换过程就是这样的:

image.png 与平台无关整型对应的就是平台相关整型,它们的长度会根据运行平台的改变而改变。Go 语言原生提供了三个平台相关整型,它们是 int、uint 与 uintptr,我同样也列了一张表:

image.png

整型的溢出问题

无论哪种整型,都有它的取值范围,也就是有它可以表示的值边界。如果这个整型因为参与某个运算,导致结果超出了这个整型的值边界,我们就说发生了整型溢出的问题。由于整型无法表示它溢出后的那个“结果”,所以出现溢出情况后,对应的整型变量的值依然会落到它的取值范围内,只是结果值与我们的预期不符,导致程序逻辑出错。比如这就是一个无符号整型与一个有符号整型的溢出情况:

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 版本还支持在字面值中增加数字分隔符“_”,分隔符可以用来将数字分组以提高可读性。比如每 3 个数字一组,也可以用来分隔前缀与字面值中的第一个数字:

a := 5_3_7   // 十进制: 537
b := 0b_1000_0111  // 二进制位表示为10000111 
c1 := 0_700  // 八进制: 0700
c2 := 0o_700 // 八进制: 0700
d1 := 0x_5c_6d // 十六进制:0x5c6d

不过,这里你要注意一下,Go 1.13 中增加的二进制字面值以及数字分隔符,只在 go.mod 中的 go version 指示字段为 Go 1.13 以及以后版本的时候,才会生效,否则编译器会报错。

反过来,我们也可以通过标准库 fmt 包的格式化输出函数,将一个整型变量输出为不同进制的形式。比如下面就是将十进制整型值 59,格式化输出为二进制、八进制和十六进制的代码:

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 既提供了 int16、int32 等类型,又有 int 类型。换句话说,Go 提供的浮点类型都是平台无关的。

无论是 float32 还是 float64,它们的变量的默认值都为 0.0,不同的是它们占用的内存空间大小是不一样的,可以表示的浮点数的范围与精度也不同。那么浮点数在内存中的二进制表示究竟是怎么样的呢?

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

image.png

我们看到浮点数在内存中的二进制表示分三个部分:符号位、阶码(即经过换算的指数),以及尾数。这样表示的一个浮点数,它的值等于:

image.png 其中浮点值的符号由符号位决定:当符号位为 1 时,浮点值为负值;当符号位为 0 时,浮点值为正值。公式中 offset 被称为阶码偏移值,这个我们待会再讲。

我们首先来看单精度(float32)与双精度(float64)浮点数在阶码和尾数上的不同。这两种浮点数的阶码与尾数所使用的位数是不一样的,你可以看下 IEEE 754 标准中单精度和双精度浮点数的各个部分的长度规定:

image.png

创建自定义的数值类型

如果我们要通过 Go 提供的类型定义语法,来创建自定义的数值类型,我们可以通过 type 关键字基于原生数值类型来声明一个新类型。

但是自定义的数值类型,在和其他类型相互赋值时容易出现一些问题。下面我们就来建立一个名为 MyInt 的新的数值类型看看:

type MyInt int32

这里,因为 MyInt 类型的底层类型是 int32,所以它的数值性质与 int32 完全相同,但它们仍然是完全不同的两种类型。根据 Go 的类型安全规则,我们无法直接让它们相互赋值,或者是把它们放在同一个运算中直接计算,这样编译器就会报错。

var m int = 5
var n int32 = 6
var a MyInt = m // 错误:在赋值中不能将m(int类型)作为MyInt类型使用
var a MyInt = n // 错误:在赋值中不能将n(int32类型)作为MyInt类型使用

我们也可以通过 Go 提供的类型别名(Type Alias)语法来自定义数值类型。和上面使用标准 type 语法的定义不同的是,通过类型别名语法定义的新类型与原类型别无二致,可以完全相互替代。我们来看下面代码:

type MyInt = int32

var n int32 = 6
var a MyInt = n // ok

你可以看到,通过类型别名定义的 MyInt 与 int32 完全等价,所以这个时候两种类型就是同一种类型,不再需要显式转型,就可以相互赋值。

此文章为3月Day2学习笔记,内容来源于极客时间《Tony Bai · Go 语言第一课》。