整数:补码的世界

Go 的 int32 能表示 −2,147,483,6482,147,483,647。为什么负数比正数多一个?因为 补码 的设计——用最高位作符号位,0 表示正数,1 表示负数,但 10000...0 这个特殊值被分给了最小负数,所以负数的绝对值范围比正数大 1。

package main

import (
    "fmt"
    "math"
)

func main() {
    // 整数溢出:不报错,只截断
    var x int32 = math.MaxInt32  // 2147483647
    x++
    fmt.Println(x)  // -2147483648  ← 溢出回绕

    // 补码:负数的二进制表示
    var n int8 = -1
    fmt.Printf("%08b\n", uint8(n))  // 11111111

    // 补码规律:正数按位取反+1 = 其负数
    // -1 的补码: 00000001 → 取反 11111110 → +1 = 11111111 ✓
}

浮点数:IEEE 754

浮点数本质是二进制科学计数法(-1)^s × 1.fraction × 2^(exponent-127)float32 用 1 位符号 + 8 位指数 + 23 位尾数,能表示的实数是稀疏分布的——越靠近 0 越密集,越大越稀疏。

package main

import (
    "fmt"
    "math"
    "math/bits"
    _ "unsafe"
)

func float64Bits(f float64) uint64 {
    return math.Float64bits(f)
}

func main() {
    // 0.1 在二进制里是无限循环小数
    fmt.Printf("0.1 bits: %064b\n", float64Bits(0.1))
    fmt.Println(0.1 + 0.2)          // 0.30000000000000004
    fmt.Println(0.1 + 0.2 == 0.3)   // false

    // 正确比较浮点数:用误差范围
    const eps = 1e-9
    a, b := 0.1+0.2, 0.3
    fmt.Println(math.Abs(a-b) < eps) // true

    // 特殊值
    inf := math.Inf(1)
    nan := math.NaN()
    fmt.Println(inf, nan, math.IsNaN(nan))

    _ = bits.OnesCount64(0) // 引入 bits 包
}

精度陷阱:为什么不用 float 存钱

package main

import (
    "fmt"
    "math/big"
)

func main() {
    // 错误做法:浮点累加
    total := 0.0
    for i := 0; i < 1000; i++ {
        total += 0.1
    }
    fmt.Println(total)  // 99.9999999999986  ← 差了 0.0000000000014

    // 正确做法 1:用整数(分/厘作单位)
    totalCents := 0
    for i := 0; i < 1000; i++ {
        totalCents += 10  // 0.1 元 = 10 分
    }
    fmt.Println(float64(totalCents) / 100)  // 100

    // 正确做法 2:math/big.Rat 精确有理数
    rat := new(big.Rat)
    tenth := new(big.Rat).SetFrac(big.NewInt(1), big.NewInt(10))
    for i := 0; i < 1000; i++ {
        rat.Add(rat, tenth)
    }
    fmt.Println(rat.FloatString(2))  // 100.00
}

数的范围速查

类型位数最小值最大值精度
int88-128127整数,精确
int3232-2,147,483,6482,147,483,647整数,精确
int6464-9.2×10¹⁸9.2×10¹⁸整数,精确
float32321.2×10⁻³⁸3.4×10³⁸约 7 位有效数字
float64642.2×10⁻³⁰⁸1.8×10³⁰⁸约 15 位有效数字
口诀金融用整数,科学用 float64,天文/加密用 math/big。
推荐做法
  • 金融金额用整数(分为单位)或 shopspring/decimal
  • 浮点比较用 math.Abs(a-b) < epsilon
  • 循环累加时考虑 Kahan 求和算法减少误差
不推荐
  • float64 存储货币金额
  • == 比较两个浮点数
  • 忽略 int 溢出——尤其是 32 位平台上的 int
常见误区
  • int 在 64 位系统是 64 位,在 32 位系统是 32 位——跨平台代码要用 int64

判断标准:能解释 0.1+0.2≠0.3 是因为尾数截断 → 掌握本章。

数学本质:数系的层次

数学上,自然数 ⊂ 整数 ⊂ 有理数 ⊂ 实数 ⊂ 复数。计算机实现了这个层次的有限离散近似uint 近似自然数,int 近似整数,float64 近似实数(但只是稀疏采样)。理解这个层次关系,你就能预测任何类型转换的行为。

计算机里没有实数,只有有限精度的近似。工程师的职责是控制这个近似的误差。

— 数值分析教材