在之前的文章中已经简单的对数据类型进行了描述,并在 2.go基础入门-基础语法与数据类型简介 中进行了简述,并列了一个表,以下对数字类型进行讲述。
整型数
整型数细分为10种,int、int8、int16、int32、int64、uint、uint8、uint16、uint32、uint64,细看可以发现,唯一的差别就是有 u 和没有 u 的区别,有 u 代表着无符号,也就是所表示的数字不包括负数,从0开始。
就拿 int8 和 uint8 做个例子讲解说明:
类型 | 大小 | bit | 十进制 |
---|---|---|---|
uint8 | 1 byte | 8 | 0到255 |
int8 | 1 byte | 8 | -128到127 |
- 怎么计算出 uint8 所能表示出的十进制呢?这个也挺简单,就是 2 的 8 次方 = 256个整数,uint8有 u 的整数,就是代表没有负数,所以从0开始到255的整数,就刚好是256个。
- int8 是无u,代表有负数的整数,256 个数,负数和正数各占一半,-128到正的127刚好256个数字。
- 其他也是这样算 int16 就是2的16次方,int32 就是2的32次方,int64 就是2的64次方,有 u 就是把数量全分配给正数,没 u 就让数量给正数和负数进行平分。
- 数字类型后面的数字int8、int16、int32、int64,这个数字代表的是bit位,1个字节8bit,所以int8占1byte;int64就是64bit,64bit除以8bit=8个byte,所以占8个字节。至此应该能看懂了吧。
特殊的 int 和 uint
int 和 uint有点特殊,它的特殊性要牢记,虽然他们并没有显式的从类型名称上提现出位数,但他们是会随着系统的位数而产生变化的,如果当前系统是32位的,那么int和uint就代表int32和int64;如果当前系统是64位的,int和uint就代表int64和uint64。
使用 int 和 uint 时必须制定位数,以防止出现不同系统间造成位数不同,而导致出错。一个程序同一个公式,在不同环境出现不一样的效果。
int除了可以表示10进制之外,还能表示二进制、八进制、十六进制的数:
- 二进制 var num int = 0b11010 前缀为 0b/0B
- 八进制 var num int = 0o15671 前缀为 0o/0O
- 十六进制 var num int = 0x172AEBF 前缀为 0x
至于进制之间的转换,有兴趣的自行百度。
整数的二进制
这里用 int8 和 uint8举个例子:
int8 和 uint8 都是 1byte 8bit
int8
uint8无符号整数 8个bit的二进制最大数是 1111 1111,2^8刚好可以表示256个数字,正数从0开始到255刚好256个数字,所以uint的最小值是0,最大值是255。
uin8
int8有符号整数 也是8个bit,不过与 uint8不同,他用1个bit用来表示符号,0表示整数,1表示负数,剩下的7个bit才是用来表示数字。
- 正数最大值表示 0111 1111,第1个bit数值0表示正数符号,符号位后7位二进制 111 1111 ,2^7可以表示128个数字,正数从0开始到127,刚好是128个数字。
- 负数最大值表示 1111 1111,第一个bit数值表示负数符号,符号位后7位二进制 111 1111,2^7可以表示128个数字,负数从-1开始到-128,刚好是127个数字。
- 由上可知,int8的数值范围是-128到127。
剩下int、uint、int16、uint16、int32、uint32、int64、uint64数值范围与上述一致。
带u的表示没符号位,全部bit都用来表示数值。
不带u的表示带符号位,最高位用来表示符号,剩下的bit位才用来表示数值。
十进制与二进制互转
我们简单讲一下十进制整数与二进制之间的互转(他们之间的互转都要使用 2 为底数)
十进制转二进制
我们拿整数 18 做个例子:
18 % 2 = 9 余 0
9 % 2 = 4 余 1
4 % 2 = 2 余 0
2 % 2 = 1 余 0
1 % 2 = 0 余 1
把18用2进行整除,直到商为=0为止,每次运算获取余数部分,逆序排列后,最终的排列结果就是整数 18 的二进制结果 10010。
二进制转十进制
我们使用上面的二进制 10010 ,这里我们还是用 2 进行运算,把整数 18 计算出来:
1 0010
我们从右往左取数运算,第一位开始从右往左的排列下标从0开始到4刚好是第五位(公式:值*底数^小表 也就是 二进制的值 * 底数2^第几位)
(第 1 位,值 0,底数 2, 下标 0) ===== 0 * 2^0 = 0
(第 2 位,值 1,底数 2, 下标 1) ===== 1 * 2^1 = 2
(第 3 位,值 0,底数 2, 下标 2) ===== 0 * 2^2 = 0
(第 4 位,值 0,底数 2, 下标 3) ===== 0 * 2^3 = 0
(第 5 位,值 1,底数 2, 下标 4) ===== 1 * 2^4 = 16
0 + 2 + 0 + 0 + 16 = 18
浮点型
什么是浮点数
浮点数是一种数据结构,他可以表示一个范围很大或者范围很小的数,但他并不精确,他只是一种表示某一范围数值的数据;比如 0.3 ,浮点数中并没有0.3这个数,只能用浮点数表示出一个非常接近0.3的数,看下面的代码
// 创建一个值为0.3的float64的变量
var f64v float64 = 0.3
// 打印输出变量的结果
fmt.Println(f64v)
fmt.Printf("%.16f\n", f64v)
fmt.Printf("%.17f\n", f64v)
fmt.Printf("%.25f\n", f64v)
/*
输出结果:
0.3
0.3000000000000000
0.29999999999999999
0.2999999999999999888977698
*/
- 使用fmt包下的Print函数打印值为 0.3 的浮点数f64v,第一行打印出0.3的字符串,所以精度没有丢失。
- 第二行打印f64v变量,格式化输出后面16位小数,打印结果0.30…0精确值。
- 第三行打印f64v变量,格式化输出后面17位小数,打印结果出现了偏差,丢失了 0.00000000000000001。
- 第四行打印f64v变量,格式化输出后面25位小数,打印的结果依旧出现了偏差。
0.3这个浮点数输出的小数位大于16位后,就开始出现精度丢失的情况,这是因为双精度浮点数能提供15-16位精确的数据,超过这个数之后就不保证精确度(下面篇幅会详细讲)。由上可以验证上述说的,他只是一种表示某一范围数值的数据。除了0.3还有很多数是不存在的,只能通过一个非常接近的值来精确到某个长度。
另一种方式表示数值
科学计数法
浮点数还有另一种表示方式,我们就拿浮点数 18.125 来进行讲解。
18.125 的另一种显示 1.8125e+1,看下面代码
// 定义两个值一样的float64变量
var v1 float64 = 18.125
var v2 float64 = 1.8125e+1
// 分别打印两个变量
fmt.Println(v1)
fmt.Println(v2)
/*
输出结果:
18.125
18.125
*/
由此可见,同一个值不同的两种表示方法,输出的结果都是 18.125,这里我们来讲一下第二种显示方式 科学计数法:
1.8125e+1,我们把这个值分为1.8125小数部分和e+1指数部分,小数部分还表明了当前数是个正数,我们下面先讲一下这个指数。
我们上面说过,浮点数之所以叫浮点数,是因为小数点是可以左右浮动的,e表示为指数,e后面的数字表示了小数点向左移还是右移?移多少位?+1表示当前小数点往右移1位,所以小数部分 1.8125 的小数点往右移1位就变成了 18.125。
它的计算公式是:S + M + E = value
- S 阶符或符号,表示当前数的正负, 1.8125很明显是个就是正数。
- M 小数, 1.8125就是一个小数(1.8125 = 阶符+小数)
- E 指数,小数点的浮动依据(+1表示右浮动1位,-1表示左浮动1位)
- 正数 + 1.8125 + 小数点有浮动1位 = 18.125
根据上面的 S M E,我们就能得到这个 科学计数法 的实际值,是不是很简单,下面我们先来讲讲 golang 中的 float32 和 float64 。
IEEE754 浮点数标准
go 中的 float34 和 float64 都是使用的IEEE754浮点数标准对数据进行存储和表示的,这里就不详细说IEEE754了,只做一下简单的讲解。
IEEE754是一种浮点数规约,就如同上面所说的科学计数法,也就是 S + M + E 表示出一个范围的值。
根据之前篇章简介说过 float34 使用4个字节进行存储,float64 使用8个字节存储。
float32 使用4个字节32个bit对数据进行存储(1个字节存储S,8个字节存储E,23个字节存储M)。
十进制小数转二进制
在讲解小数前,我们先讲一下如何将十进制的小数转换成二进制,我们之前讲过整数部分是用结果整除2取余进行逆排序;小数也非常简单,用小数部分乘以2,取积的整数部分进行正排序,直到积等于1或者到达存储精度为止。
浮点数 18.125,小数部分 0.125
0.125 * 2 = 0.25
0.25 * 2 = 0.5
0.5 * 2 = 1
正序取积整数部分 那么小数部分的二进制就是 0.001,我们可以去进制在线网页进行进制转换验证。
有一些小数是不能被准确表示的,就如上面说的 0.3,积永远都不可能为1,float32 他是用23个bit来存储小数的,从二进制小数出现第一个 1 开始,往后面再计算再取23次整数就可以停止运算了,详细往下看。
0.3 * 2 = 0.6
0.6 * 2 = 1.2
0.2 * 2 = 0.4
0.4 * 2 = 0.8
0.8 * 2 = 1.6
0.6 * 2 = 1.2
…
01 后面算出23位就可以停止了,最终结果如下:
0.0100110011001100110011001
使用二进制小数转换成十进制小数的时候,只得到0.2999999… ,上面也说过浮点数是没有0.3的,实际存储的只是一个很接近0.3的二进制小数,显示时被根据位数精确到了 0.3。
二进制小数转十进制小数
整数部分是根据第0位一次往后 进行运算(二进制值 * 底数2^第几位)),小数也一样,不过小数部分的属性不是0正序开始,而是负数 -1开始。
0.0100110011001100110011001
0
IEEE754标准的 小数: 我们上面已经讲过了十进制整数转二进制、十进制小数转二进制,我们先拿0.3 作一个举例。
-
0.3 的二进制小数(第一个1后23位) 0.0100110011001100110011001
-
0.01 小数点浮动到1前面需要向右移动2位,此时E = -2(浮动数,小数点右浮动以负数表示,左移动正数表示,左浮动2所以是-2) 001.00110011001100110011001
现在小数点前面肯定是1.开头,我们把浮动步数记录到 E 中,把小数点右边截出来刚好是23位。
float32 的小数部分是用23个bit进行存储的,刚好把截取出来 00110011001100110011001 存储进这23个bit中,那么这部分就是M了,因为第一位永远的1.xx所以IEEE754标准中是不存储 1. 部分的,读取的时候把 1. + M,然后用 E的浮动数就可以把原来的小数还原出来了。
1.+ 00110011001100110011001
根据E的步数往左边移动2位:
0.0100110011001100110011001
(第 1 位,值 0,底数 2, 下标 -1) ===== 0 * 2^-1 = 0
(第 2 位,值 1,底数 2, 下标 -2) ===== 1 * 2^-2 = 0.25
(第 3 位,值 0,底数 2, 下标 -3) ===== 0 * 2^-3 = 0
(第 4 位,值 0,底数 2, 下标 -4) ===== 0 * 2^-4 = 0
(第 5 位,值 1,底数 2, 下标 -5) ===== 1 * 2^-5 = 0.03125
…
(第 25 位,值 1,底数 2, 下标 -26) ===== 1 * 2^-5 = 2.9802322387695312e-08
把所有的结果相加就能得到一个最终值了:
0+0.25+0+0+0.3125+…+2.9802322387695312e-08=0.2999999…,一个接近于0.3的数值。
0.3的E是-2右移两位,那什么情况下小数点左移呢?
答案是带整数的浮点数,我们用 18.125 来列举:
(我们用上面所学的先把整数部分和小数部分都分别转成二进制)
- 整数部分18 上述篇幅中我们算出来的二进制是 10010
- 小数部分0.125 上述篇幅中我们算出来的二进制是 001
- 整数二进制10010加上小数001部分,18.125=10010.001
10010.001 在0.3的案例中讲过,要计算出M,就要先把小数点浮动到第一个1后面,所以小数点浮动到第一个1后面需要左移4,右移是负数,那么左移E=4
1.0010001
上面说过,小数点前的1,在存储时是省略掉的,只存储小数点后面的23位,所以需要补0,填满23个bit。
M=001 0001 0000 0000 0000 0000
S(0) E(4) M(001 0001 0000 0000 0000 0000)
这里的SEM能看懂了吧?
S代表符号,E代表浮点数的移动,M代表小数。
E 阶码或指数
我们需要讲一下这个 指数,我们前面说过,这个 E 存储的是浮点数的浮动方向和位数,如上面的 18.125转换后的E是4,正数表示左浮动,所以是左浮动4位。而实际上我们的E存储的并不是4。
前面说过,我们E部分是使用8个bit无符号进行存储的,所表示的范围是 2^8=256个数字,数字0到255刚好是256个数,但我们的浮动数阶码是用正负数表示移动方向和数量的,而0-255是不包含正数的,比如 -4 就不能直接存储了。
0.xxxxx…
如果尾数就是0.xxxxx…,那么他的阶码取值范围在 -128到127;
但是IEEE754中,我们完整的小数M部分是1.xxxx…,取值范围-127到128;第一个值和最后一个值是用来表示特殊值的,所以我们需要把前后各拿掉一个数值用来表示特殊值,这时候取值范围是 -126到127。
上面也说过,阶码E实际上存储的是无符号8个bit的二进制,其最大能表示256个数值,所以E实际存储的0到255这个范围的数值,然后我们拿掉头尾后,就是1到254;-126对应无符号二进制取值1,127对应无符号二进制取值254。
那么我们的 E 阶码,实际存储的值是 1到254,虽然我们知道实际存值的126=-1,但是程序不知道,所以我们需要把实际存储的126转换成-1,这时候就要讲到 偏移量了。
偏移量是固定的 (2^Ebit-1)-1,Ebit指的就是指数的bit 8个,(2^8-1)-1 = 127,我们的固定偏移量就是127;双精度 float64 中,E是用11bit存储的,所以 float64的偏移量是 (2^11-1)-1。
转换过程:
- 18.125
- 符号S = 0,表示正数
- 18.125 转二进制 10010.001
- 左移4,e=4,1.0010001舍去1.并补0,M=001 0001 0000 0000 0000 0000
- 指数E = 偏移量 + e = 4 + 127 = 131 转二进制 10000011
上述中,我们得知左移4位,存储时并非是直接存储移码4,而是存储一个 移码+偏移量,范围在1到254之间的值,此时我们已经拿到了 S E M的值。
S + E + M = 0 10000011 001 0001 0000 0000 0000 0000
这就是float32类型浮点数 18.125 实际存储的数据。
读取时只要用 E转十进制 - 偏移量 = 实际移码,S+1.+ M+E,通过E的移码得到一个浮点数二进制,再通过二进制计算转十进制就能得到原值 18.125了。
说了那么多,其实就是几个东西:
- S符号 表示正负数
- M小数 = 整数二进制 + 小数二进制 → 小数点移动e → 截取1.
- E指数(1-254) = e移码(-126到127) + 偏移量((2^Ebit-1)-1)
浮点数的数据结构就是这些S E M,最核心的就是指数E部分,因为浮点数的表示都是根据E指数进行小数点的移动的。如果对这个有兴趣,可以去百度搜索IEEE754阅读一些比较专业的文章。
精度、最大值、最小值
我们上面提到过单精度 float32精度是6-7位,有效数字为7位,小数部分只能精确到6位。那么这个精度是怎么来的呢?其实也很简单,一个公式 2^-Mbit = 1.1920928955078125e-07,十进制1.19*10^-7,所以能提供大概7位的有效数字。float64的精度是15-16位也是用这公式可以计算出来,这里就不再讲述了。
浮点数所能表示的最大值和最小值,我们下面用 float32 来说一下,还记的我们上面讲的 指数E 部分吗? 移码的范围是 -126到127,还记得吗?
2^-126 就是 float32所能表示的最小值。
2^127 就是 float32所能表示的最大值。
至此我们对浮点型已经讲完了。