You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

373 lines
23 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 12基本数据类型Go原生支持的数值类型有哪些
你好我是Tony Bai。
在上一课中我们学习了Go变量的声明形式知道了变量所绑定的内存区域应该有明确的边界而这个边界信息呢是由变量的类型赋予的。那么顺着这个脉络从这一节课开始我们就来深入讲解Go语言类型。
你可能会有点不解,类型是每个语言都有的东西,我们有必要花那么多时长、讲那么详细吗?
有必要。对像Go这样的静态编程语言来说类型是十分重要的。因为它不仅是静态语言编译器的要求更是我们对现实事物进行抽象的基础。对这一方面的学习可以让你逐渐建立起代码设计的意识。
Go语言的类型大体可分为基本数据类型、复合数据类型和接口类型这三种。其中我们日常Go编码中使用最多的就是基本数据类型而基本数据类型中使用占比最大的又是数值类型。
那么我们今天就先来讲数字类型。Go语言原生支持的数值类型包括整型、浮点型以及复数类型它们适用于不同的场景。我们依次来看一下。
## 被广泛使用的整型
Go语言的整型主要用来表示现实世界中整型数量比如人的年龄、班级人数等。它可以分为**平台无关整型**和**平台相关整型**这两种它们的区别主要就在这些整数类型在不同CPU架构或操作系统下面它们的长度是否是一致的。
我们先来看**平台无关整型**它们在任何CPU架构或任何操作系统下面长度都是固定不变的。我在下面这张表中总结了Go提供的平台无关整型
![图片](https://static001.geekbang.org/resource/image/06/f6/06b6f40dd25ed4296b5bae6fa8d890f6.jpg?wh=1920x1047)
你可以看到这些平台无关的整型也可以分成两类有符号整型int8~int64和无符号整型uint8~uint64。两者的本质差别在于最高二进制位bit位是否被解释为符号位这点会影响到无符号整型与有符号整型的取值范围。
我们以下图中的这个8比特一个字节的整型值为例当它被解释为无符号整型uint8时和它被解释为有符号整型int8时表示的值是不同的
![图片](https://static001.geekbang.org/resource/image/0b/26/0b9890673b05aa168b7bf6031d7f6926.jpg?wh=1611x859)
在同样的比特位表示下当最高比特位被解释为符号位时它代表一个有符号整型int8它表示的值为-127当最高比特位不被解释为符号位时它代表一个无符号整型(uint8)它表示的值为129。
这里你可能就会问了:即便最高比特位被解释为符号位,上面的有符号整型所表示值也应该为-1啊怎么会是-127呢
这是因为Go采用**2的补码**Twos Complement作为整型的比特位编码方法。因此我们不能简单地将最高比特位看成负号把其余比特位表示的值看成负号后面的数值。Go的补码是通过原码逐位取反后再加1得到的比如我们以-127这个值为例它的补码转换过程就是这样的
![图片](https://static001.geekbang.org/resource/image/5b/3c/5bfe6c399f9cb192dd3cc43f180e4b3c.jpg?wh=1731x907)
与平台无关整型对应的就是平台相关整型它们的长度会根据运行平台的改变而改变。Go语言原生提供了三个**平台相关整型**它们是int、uint与uintptr我同样也列了一张表
![图片](https://static001.geekbang.org/resource/image/bb/e3/bb54f92e201e950e7977f98d2be52fe3.jpg?wh=1731x907)
在这里我们要特别注意一点,由于这三个类型的长度是平台相关的,所以我们**在编写有移植性要求的代码时,千万不要强依赖这些类型的长度**。如果你不知道这三个类型在目标运行平台上的长度可以通过unsafe包提供的SizeOf函数来获取比如在x86-64平台上它们的长度均为8
```plain
var a, b = int(5), uint(6)
var p uintptr = 0x12345678
fmt.Println("signed integer a's length is", unsafe.Sizeof(a)) // 8
fmt.Println("unsigned integer b's length is", unsafe.Sizeof(b)) // 8
fmt.Println("uintptr's length is", unsafe.Sizeof(p)) // 8
```
现在我们已经搞清楚Go语言中整型的分类和长度了但是在使用整型的过程中我们还会遇到一个常见问题整型溢出。
### 整型的溢出问题
无论哪种整型,都有它的取值范围,也就是有它可以表示的值边界。如果这个整型因为参与某个运算,导致结果超出了这个整型的值边界,我们就说发生了**整型溢出**的问题。由于整型无法表示它溢出后的那个“结果”,所以出现溢出情况后,对应的整型变量的值依然会落到它的取值范围内,只是结果值与我们的预期不符,导致程序逻辑出错。比如这就是一个无符号整型与一个有符号整型的溢出情况:
```plain
var s int8 = 127
s += 1 // 预期128实际结果-128
var u uint8 = 1
u -= 2 // 预期-1实际结果255
```
你看有符号整型变量s初始值为127在加1操作后我们预期得到128但由于128超出了int8的取值边界其实际结果变成了-128。无符号整型变量u也是一样的道理它的初值为1在进行减2操作后我们预期得到-1但由于-1超出了uint8的取值边界它的实际结果变成了255。
这个问题最容易发生在循环语句的结束条件判断中,因为这也是经常使用整型变量的地方。无论无符号整型,还是有符号整型都存在溢出的问题,所以我们要十分小心地选择参与循环语句结束判断的整型变量类型,以及与之比较的边界值。
在了解了整型的这些基本信息后,我们再来看看整型支持的不同进制形式的字面值,以及如何输出不同进制形式的数值。
### 字面值与格式化输出
Go语言在设计开始就继承了C语言关于**数值字面值Number Literal**的语法形式。早期Go版本支持十进制、八进制、十六进制的数值字面值形式比如
```plain
a := 53 // 十进制
b := 0700 // 八进制,以"0"为前缀
c1 := 0xaabbcc // 十六进制,以"0x"为前缀
c2 := 0Xddeeff // 十六进制,以"0X"为前缀
```
Go 1.13版本中Go又增加了对二进制字面值的支持和两种八进制字面值的形式比如
```plain
d1 := 0b10000001 // 二进制,以"0b"为前缀
d2 := 0B10000001 // 二进制,以"0B"为前缀
e1 := 0o700 // 八进制,以"0o"为前缀
e2 := 0O700 // 八进制,以"0O"为前缀
```
为提升字面值的可读性Go 1.13版本还支持在字面值中增加数字分隔符“\_”分隔符可以用来将数字分组以提高可读性。比如每3个数字一组也可以用来分隔前缀与字面值中的第一个数字
```plain
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格式化输出为二进制、八进制和十六进制的代码
```plain
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
```
到这里,我们对整型的学习就先告一段落了。我们接下来看另外一个数值类型:浮点型。
## 浮点型
和使用广泛的整型相比浮点型的使用场景就相对聚焦了主要集中在科学数值计算、图形图像处理和仿真、多媒体游戏以及人工智能等领域。我们这一部分对于浮点型的学习主要是讲解Go语言中浮点类型在内存中的表示方法这可以帮你建立应用浮点类型的理论基础。
### 浮点型的二进制表示
要想知道Go语言中的浮点类型的二进制表示是怎样的我们首先要来了解[IEEE 754标准](https://zh.wikipedia.org/wiki/IEEE_754)。
IEEE 754是IEEE制定的二进制浮点数算术标准它是20世纪80年代以来最广泛使用的浮点数运算标准被许多CPU与浮点运算器采用。现存的大部分主流编程语言包括Go语言都提供了符合IEEE 754标准的浮点数格式与算术运算。
IEEE 754标准规定了四种表示浮点数值的方式单精度32位、双精度64位、扩展单精度43比特以上与扩展双精度79比特以上通常以80位实现。后两种其实很少使用我们重点关注前面两个就好了。
Go语言提供了float32与float64两种浮点类型它们分别对应的就是IEEE 754中的单精度与双精度浮点数值类型。不过这里要注意Go语言中没有提供float类型。这不像整型那样Go既提供了int16、int32等类型又有int类型。换句话说Go提供的浮点类型都是平台无关的。
那float32与float64这两种浮点类型有什么异同点呢
无论是float32还是float64它们的变量的默认值都为0.0,不同的是它们占用的内存空间大小是不一样的,可以表示的浮点数的范围与精度也不同。那么浮点数在内存中的二进制表示究竟是怎么样的呢?
浮点数在内存中的二进制表示Bit Representation要比整型复杂得多IEEE 754规范给出了在内存中存储和表示一个浮点数的标准形式见下图
![图片](https://static001.geekbang.org/resource/image/71/59/71ec0ab2749d077eb8ae1451f7823859.jpg?wh=1731x718)
我们看到浮点数在内存中的二进制表示分三个部分:符号位、阶码(即经过换算的指数),以及尾数。这样表示的一个浮点数,它的值等于:
![图片](https://static001.geekbang.org/resource/image/d5/27/d5bd785155a998bccd0e913f66024e27.jpg?wh=1100x307)
其中浮点值的符号由符号位决定当符号位为1时浮点值为负值当符号位为0时浮点值为正值。公式中offset被称为阶码偏移值这个我们待会再讲。
**我们首先来看单精度float32与双精度float64浮点数在阶码和尾数上的不同。**这两种浮点数的阶码与尾数所使用的位数是不一样的你可以看下IEEE 754标准中单精度和双精度浮点数的各个部分的长度规定
![图片](https://static001.geekbang.org/resource/image/0b/ce/0b6c1bf3d12cf270e29a79db0c342bce.jpg?wh=1731x907)
我们看到单精度浮点类型float32为符号位分配了1个bit为阶码分配了8个bit剩下的23个bit分给了尾数。而双精度浮点类型除了符号位的长度与单精度一样之外其余两个部分的长度都要远大于单精度浮点型阶码可用的bit位数量为11尾数则更是拥有了52个bit位。
**接着,我们再来看前面提到的“阶码偏移值”,我想用一个例子直观地让你感受一下。**在这个例子中我们来看看如何将一个十进制形式的浮点值139.8125转换为IEEE 754规定中的那种单精度二进制表示。
**步骤一:我们要把这个浮点数值的整数部分和小数 部分,分别转换为二进制形式**后缀d表示十进制数后缀b表示二进制数
* 整数部分139d => 10001011b
* 小数部分0.8125d => 0.1101b十进制小数转换为二进制可采用“乘2取整”的竖式计算
这样,原浮点值**139.8125d**进行二进制转换后,就变成**10001011.1101b**。
**步骤二移动小数点直到整数部分仅有一个1**也就是10001011.1101b => 1.00010111101b。我们看到为了整数部分仅保留一个1小数点向左移了7位这样指数就为7尾数为00010111101b。
**步骤三:计算阶码。**
IEEE754规定不能将小数点移动得到的指数直接填到阶码部分指数到阶码还需要一个转换过程。对于float32的单精度浮点数而言阶码 = 指数 + 偏移值。偏移值的计算公式为2^(e-1)-1其中e为阶码部分的bit位数这里为8于是**单精度浮点数的阶码偏移值**就为2^(8-1)-1 = 127。这样在这个例子中阶码 = 7 + 127 = 134d = 10000110b。float64的双精度浮点数的阶码计算也是这样的。
**步骤四:将符号位、阶码和尾数填到各自位置,得到最终浮点数的二进制表示。**尾数位数不足23位可在后面补0。
![图片](https://static001.geekbang.org/resource/image/f9/a7/f9292fe5bdc5f24938bab691ea5451a7.jpg?wh=1731x566)
这样,最终浮点数**139.8125d**的二进制表示就为**0b\_0\_10000110\_00010111101\_000000000000**。
最后我们再通过Go代码输出浮点数**139.8125d**的二进制表示,和前面我们手工转换的做一下比对,看是否一致。
```plain
func main() {
var f float32 = 139.8125
bits := math.Float32bits(f)
fmt.Printf("%b\n", bits)
}
```
在这段代码中我们通过标准库的math包将float32转换为整型。在这种转换过程中float32的内存表示是不会被改变的。然后我们再通过前面提过的整型值的格式化输出将它以二进制形式输出出来。运行这个程序我们得到下面的结果
```
1000011000010111101000000000000
```
我们看到这个值在填上省去的最高位的0后与我们手工得到的浮点数的二进制表示一模一样。这就说明我们手工推导的思路并没有错。
而且你可以从这个例子中感受到阶码和尾数的长度决定了浮点类型可以表示的浮点数范围与精度。因为双精度浮点类型float64阶码与尾数使用的比特位数更多它可以表示的精度要远超单精度浮点类型所以在日常开发中我们使用双精度浮点类型float64的情况更多这也是Go语言中浮点常量或字面值的默认类型。
而float32由于表示范围与精度有限经常会给开发者造成一些困扰。比如我们可能会因为float32精度不足导致输出结果与常识不符。比如下面这个例子就是这样f1与f2两个浮点类型变量被两个不同的浮点字面值初始化但逻辑比较的结果却是两个变量的值相等。至于其中原因我将作为思考题留给你你可以结合前面讲解的浮点类型表示方法对这个例子进行分析
```plain
var f1 float32 = 16777216.0
var f2 float32 = 16777217.0
fmt.Println(f1 == f2) // true
```
看到这里,你是不是觉得浮点类型很神奇?和易用易理解的整型相比,浮点类型无论在二进制表示层面,还是在使用层面都要复杂得多。即便是浮点字面值,有时候也不是一眼就能看出其真实的浮点值是多少的。下面我们就接着来分析一下浮点型的字面值。
### 字面值与格式化输出
Go浮点类型字面值大体可分为两类一类是**直白地用十进制表示的浮点值形式**。这一类,我们通过字面值就可直接确定它的浮点值,比如:
```plain
3.1415
.15 // 整数部分如果为0整数部分可以省略不写
81.80
82. // 小数部分如果为0小数点后的0可以省略不写
```
另一类则是**科学计数法形式**。采用科学计数法表示的浮点字面值,我们需要通过一定的换算才能确定其浮点值。而且在这里,科学计数法形式又分为十进制形式表示的,和十六进制形式表示的两种。
我们先来看十进制科学计数法形式的浮点数字面值这里字面值中的e/E代表的幂运算的底数为10
```plain
6674.28e-2 // 6674.28 * 10^(-2) = 66.742800
.12345E+5 // 0.12345 * 10^5 = 12345.000000
```
接着是十六进制科学计数法形式的浮点数:
```plain
0x2.p10 // 2.0 * 2^10 = 2048.000000
0x1.Fp+0 // 1.9375 * 2^0 = 1.937500
```
这里我们要注意十六进制科学计数法的整数部分、小数部分用的都是十六进制形式但指数部分依然是十进制形式并且字面值中的p/P代表的幂运算的底数为2。
知道了浮点型的字面值后和整型一样fmt包也提供了针对浮点数的格式化输出。我们最常使用的格式化输出形式是%f。通过%f我们可以输出浮点数最直观的原值形式。
```plain
var f float64 = 123.45678
fmt.Printf("%f\n", f) // 123.456780
```
我们也可以将浮点数输出为科学计数法形式,如下面代码:
```plain
fmt.Printf("%e\n", f) // 1.234568e+02
fmt.Printf("%x\n", f) // 0x1.edd3be22e5de1p+06
```
其中%e输出的是十进制的科学计数法形式而%x输出的则是十六进制的科学计数法形式。
到这里,关于浮点类型的内容就告一段落了。有了整型和浮点型的基础,接下来我们再进行复数类型的学习就容易多了。
## 复数类型
数学课本上将形如z=a+bia、b均为实数a称为实部b称为虚部的数称为**复数**这里我们也可以这么理解。相比C语言直到采用C99标准才在complex.h中引入了对复数类型的支持Go语言则原生支持复数类型。不过和整型、浮点型相比复数类型在Go中的应用就更为局限和小众主要用于专业领域的计算比如矢量计算等。我们简单了解一下就可以了。
Go提供两种复数类型它们分别是complex64和complex128complex64的实部与虚部都是float32类型而complex128的实部与虚部都是float64类型。如果一个复数没有显示赋予类型那么它的默认类型为complex128。
关于复数字面值的表示,我们其实有三种方法。
**第一种,我们可以通过复数字面值直接初始化一个复数类型变量:**
```plain
var c = 5 + 6i
var d = 0o123 + .12345E+5i // 83+12345i
```
**第二种Go还提供了complex函数方便我们创建一个complex128类型值**
```plain
var c = complex(5, 6) // 5 + 6i
var d = complex(0o123, .12345E+5) // 83+12345i
```
**第三种你还可以通过Go提供的预定义的函数real和imag来获取一个复数的实部与虚部返回值为一个浮点类型**
```plain
var c = complex(5, 6) // 5 + 6i
r := real(c) // 5.000000
i := imag(c) // 6.000000
```
至于复数形式的格式化输出的问题由于complex类型的实部与虚部都是浮点类型所以我们可以直接运用浮点型的格式化输出方法来输出复数类型你直接参考前面的讲解就好了。
到这里其实我们已经把Go原生支持的数值类型都讲完了。但是在原生数值类型不满足我们对现实世界的抽象的情况下你可能还需要通过Go提供的类型定义语法来创建自定义的数值类型这里我们也适当延展一下看看这种情况怎么做。
## 延展:创建自定义的数值类型
如果我们要通过Go提供的类型定义语法来创建自定义的数值类型我们可以通过type关键字基于原生数值类型来声明一个新类型。
但是自定义的数值类型在和其他类型相互赋值时容易出现一些问题。下面我们就来建立一个名为MyInt的新的数值类型看看
```plain
type MyInt int32
```
这里因为MyInt类型的底层类型是int32所以它的数值性质与int32完全相同但它们仍然是完全不同的两种类型。根据Go的类型安全规则我们无法直接让它们相互赋值或者是把它们放在同一个运算中直接计算这样编译器就会报错。
```
var m int = 5
var n int32 = 6
var a MyInt = m // 错误在赋值中不能将mint类型作为MyInt类型使用
var a MyInt = n // 错误在赋值中不能将nint32类型作为MyInt类型使用
```
要避免这个错误,我们需要借助**显式转型**,让赋值操作符左右两边的操作数保持类型一致,像下面代码中这样做:
```plain
var m int = 5
var n int32 = 6
var a MyInt = MyInt(m) // ok
var a MyInt = MyInt(n) // ok
```
**我们也可以通过Go提供的类型别名Type Alias语法来自定义数值类型**。和上面使用标准type语法的定义不同的是通过类型别名语法定义的新类型与原类型别无二致可以完全相互替代。我们来看下面代码
```plain
type MyInt = int32
var n int32 = 6
var a MyInt = n // ok
```
你可以看到通过类型别名定义的MyInt与int32完全等价所以这个时候两种类型就是同一种类型不再需要显式转型就可以相互赋值。
## 小结
好了,今天的课讲到这里就结束了,现在我们一起来回顾一下吧。
在这一讲中我们开始学习Go的数据类型了。我们从最简单且最常用的数值类型开始学起。Go的原生数值类型有三类整型、浮点型和复数型。
首先,整数类型包含的具体类型比较多,我这里用一个表格做个总结:
![图片](https://static001.geekbang.org/resource/image/8d/a7/8dab473d89930744d721dfa48e9723a7.jpg?wh=1637x1119)
Go语言中的整型的二进制表示采用2的补码形式你可以回忆一下如何计算一个负数的补码其实很简单记住“原码取反加1”即可。
另外,学习整型时你要特别注意,每个整型都有自己的取值范围和表示边界,一旦超出边界,便会出现溢出问题。溢出问题多出现在循环语句中进行结束条件判断的位置,我们在选择参与循环语句结束判断的整型变量类型以及比较边界值时要尤其小心。
接下来我们还讲了Go语言实现了IEEE 754标准中的浮点类型二进制表示。在这种表示中一个浮点数被分为符号位、阶码与尾数三个部分我们用一个实例讲解了如何推导出一个浮点值的二进制表示。如果你理解了那个推导过程你就基本掌握浮点类型了。虽然我们在例子中使用的是float32类型做的演示但日常使用中我们尽量使用float64这样不容易出现浮点溢出的问题。复数类型也是基于浮点型实现的日常使用较少你简单了解就可以了。
最后,我们还了解了如何利用类型定义语法与类型别名语法创建自定义数值类型。通过类型定义语法实现的自定义数值类型虽然在数值性质上与原类型是一致的,但它们却是完全不同的类型,不能相互赋值,比如通过显式转型才能避免编译错误。而通过类型别名创建的新类型则等价于原类型,可以互相替代。
## 思考题
今天的思考题我想请你分析一下下面例子中f1为何会与f2相等欢迎在留言区留下你的答案。
```plain
var f1 float32 = 16777216.0
var f2 float32 = 16777217.0
f1 == f2 // true
```
欢迎把这节课分享给更多对Go语言感兴趣的朋友。我是Tony Bai我们下节课见。