Go 语言类型 浮点型

基本概念

浮点型数据(Floating-point-number)用于表示包含小数部分的数值,也称为实数(Real-number)。

浮点类型

Go 语言提供两种浮点类型 float32float64。这两种类型遵循 IEEE-754 标准,其中 float32 是单精度浮点数,float64 是双精度浮点数:

类型 字节长度 精确位数
float32 4 小数点后 7 位
float64 8 小数点后 15 位

单精度浮点数在运算时会迅速积累误差,应优先使用双精度浮点数。

表示方法

浮点型数据在 Go 语言中有两种表示方式:

  • 十进制小数:由数字和小数点组成,例如:0.210
  • 指数形式:由数字和字母 e 组成,例如表示 1234.5 指数形式为 12.345e21.2345e3

浮点结构

浮点数在计算机中表示基于科学记数法(value=signmantissabaseexponentvalue = sign * mantissa * base^{exponent}),在内存中按指数形式储存,分为三部分:符号位(Sign)、指数(Exponent)和尾数(Mantissa):

  • 符号位:用于表示数值正负的 1 个单独位,0 表示正数,1 表示负数。
  • 指数:用于表示基数的幂。不是直接存储实际指数值,而是存储一个偏移指数,有利于表达正负。例如,在 float32 类型中,指数部分占用 8 位,偏移量为 127,实际指数值是存储的指数值 - 偏移量。如果存储的指数值是 120,那么实际指数值是 120-127=-7
  • 尾数:用于表示有效数字或分数部分。尾数部分用二进制表示为 1.xxxx 形式,其中 1 默认隐藏(对于规格化的数),不占用存储空间。实际存储值是 xxxx,也就是小数部分。在 float32 中,尾数部分占用 23 位,加上隐含的 1,总共可以提供 24 位精度。

在十进制中,科学记数法这样表示:±d.d...d * 10^n,其中 d.d...d 是尾数部分,基数为 10,而 n 是指数。在二进制中,基数变成了 2。假设有一个 32 位的 IEEE 754 浮点数:

  • 符号位0,代表是正数。
  • 指数10000011 等于十进制 131,表示实际指数 4
  • 尾数1010000...(省略后面许多 0,注意隐含前导 1)。

这个数值是 1.101 * 2^4,转换成十进制大约是 1.625 * 16 = 26

反过来,对一个单精度浮点数 6.75 转为科学计数法:

  • 符号位0
  • 指数:实际指数为 2,加上偏移量 127 等于 129,二进制表示为 10000001
  • 尾数:将 6.75 转为二进制浮点数等于 110.11,规范化表示为 1.1011 * 2^2,取小数点后部分 1011,储存时补齐到 23 位就是 10110000...

下面是用代码演示推导过程:

package main

import (
	"fmt"
	"math"
)

func main() {
	// 定义一个单精度浮点数
	var f float32 = 6.75

	// 将浮点数转换为其二进制表示的 uint32 类型
	bits := math.Float32bits(f)

	// 提取符号位、指数部分和尾数部分
	sign := bits >> 31
	exponent := (bits >> 23) & 0xFF
	mantissa := bits & 0x7FFFFF

	fmt.Printf("符号位: %d\n", sign)                        // 输出:0
	fmt.Printf("指数部分: %d (%08b)\n", exponent, exponent)  // 输出:129 (10000001)
	fmt.Printf("尾数部分: %d (%023b)\n", mantissa, mantissa) // 输出:5767168 (10110000000000000000000)
}

这种存储方式使得浮点数能表示非常宽泛的数值,但由于指数和尾数部分储存位数有限,有时候会导致精度问题。例如,0.1 用二进制表示开始为 0.000110011...,后面的 0011 会无限循环,直到达到精度限制。大部分十进制小数在二进制中是无限循环小数,因此在计算机中不能被精确表示。

声明和初始化

浮点数字面量自动类型推断为 float64,有下面 4 种声明和初始化方式:

package main

import "fmt"

func main() {
	// 声明但不赋值,初始化零值为 0.0
	var a float64

	// 显式声明并赋值
	var b float32 = 4

	// 短变量声明,自动推导类型为 float64,值为 5.10
	c := 5.09999999999999999999999

	// 表达式赋值,强调类型
	d := float32(3)

	// 打印变量类型和值
	fmt.Printf("类型为:%T\t%T\t%T\t%T\n", a, b, c, d)
	fmt.Printf("浮点值:%0.1f\t%.1f\t%.2f\t%0.2f\n", a, b, c, d)
}

浮点运算

可以对浮点数进行除了 %(要使用 math.Mod) 以外任意数学和比较运算,但计算结果有误差:

package main

import "fmt"

func main() {
	var a, b float32 = 5.01, 3.1
	fmt.Println("32 位浮点数加法:", a+b) // 输出:8.110001
	fmt.Println("32 位浮点数减法:", a-b) // 输出:1.9100003
	fmt.Println("32 位浮点数乘法:", a*b) // 输出:15.531
	fmt.Println("32 位浮点数除法:", a/b) // 输出:1.6161292

	var c, d float64 = 5.01, 3.1
	fmt.Println("\n64 位浮点数加法:", c+d) // 输出:8.11
	fmt.Println("64 位浮点数减法:", c-d)   // 输出:1.9099999999999997
	fmt.Println("64 位浮点数乘法:", c*d)   // 输出:15.531
	fmt.Println("64 位浮点数除法:", c/d)   // 输出:1.6161290322580644
}

从上面结果可以看出,精度越高误差越小。

舍入误差

float64 类型变量值小数位数超过 15 位时,超出部分会被舍去。同理,float32 类型在小数位数超过 7 位时会发生截断,因此不要直接比较两个浮点数相等性:

package main

import "fmt"

func main() {
	// 定义两个 float64 类型变量,尝试赋予 b 超出 float64 精度的值
	a, b := 1.0, 1.0000000000000001

	// 检查两个浮点数是否相等。由于精度限制,b 被视为与 a 相等
	if a == b {
		fmt.Printf("a 等于 b\na 的值为:%v\nb 的值为:%v\n", a, b)
	}
}

导致浮点数错误的极限值称为机器精度(machine epsilon)。高精度科学计算中,应当使用 math 标准库中相关功能来避免舍入误差。

特殊值

除法运算中,除数为浮点数 0,结果会得到特殊值正无穷大(+Inf)、负无穷大(-Inf)和非数(NaN):

package main

import (
	"fmt"
	"math"
)

func main() {
	b := float64(0)

	// 正数除以浮点数 0,结果为正无穷大
	fmt.Println("正无穷大:", 1.1/b, math.Inf(1)) // 输出:+Inf

	// 负数除以浮点数 0,结果为负无穷大
	fmt.Println("负无穷大:", -1.1/b, math.Inf(-1)) // 输出:-Inf

	// 被除数和为 0,结果为 Not a Number
	fmt.Println("非数:", 0/b, math.NaN()) // 输出:NaN
}