第9章:随机数——math/rand、crypto/rand

第9章:随机数——math/rand、crypto/rand

“在计算机世界里,没有真正的随机数,只有足够复杂的伪随机数。” ——一位被骰子欺骗的程序员

随机数是现代计算的基石。从掷骰子游戏到密码学密钥,从蒙特卡洛模拟到机器学习,数据都离不开随机数。但你知道吗?计算机里的"随机数"其实都是"算计"出来的!本章带你揭开 Go 语言随机数包的神秘面纱。


9.1 随机数包解决什么问题

想象你是一位游戏开发者和密码学工程师,你会遇到这样的困境:

🎲 游戏开发者: 我需要"随机"骰子!

  • 掷出 1~6,概率均等
  • 每次结果不同,但可以复现(方便调试)
  • 性能要快,毕竟每秒要掷几万次

📊 模拟工程师: 我需要"统计"分布!

  • 身高、体重、考试成绩——这些都符合正态分布
  • 客户到达间隔时间——指数分布
  • 热点数据访问频率——Zipf 分布

🔐 安全工程师: 我需要"不可预测"的密钥!

  • 攻击者就算知道了之前的密钥,也猜不出下一个
  • 必须来自物理世界的真实熵源
  • 速度慢一点没关系,安全性是第一位

Go 贴心地准备了两套工具:

场景特点
游戏、模拟math/rand速度快,可复现,伪随机
密钥、Tokencrypto/rand不可预测,密码学安全,慢
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
    "fmt"
    "math/rand"
    crypto_rand "crypto/rand"
)

func main() {
    // math/rand: 游戏骰子,秒生成
    fmt.Printf("🎲 随机骰子: %d\n", rand.Intn(6)+1) // 1~6

    // crypto/rand: 密钥,安全性拉满
    b := make([]byte, 8)
    crypto_rand.Read(b)
    fmt.Printf("🔐 安全随机字节: %x\n", b)
}

专业词汇解释:

  • 熵(Entropy): 物理世界的随机程度。操作系统通过键盘敲击时间、鼠标移动、硬件噪声等方式收集熵。
  • 伪随机数(Pseudorandom Number): 由确定性的算法生成,看起来随机但实际可预测。
  • 真随机数(True Random Number): 来自物理现象,不可预测。

9.2 随机数核心原理

math/rand:伪随机数的"套路"

伪随机数的原理很简单:用一个种子(Seed)通过数学公式计算出看似随机序列

1
2
// 伪随机数生成器的工作原理(简化版)
// 公式: 下一个数 = (a * 当前数 + c) mod m

给定相同的种子,生成器会吐出一模一样的序列——这在调试和测试时简直是救命稻草!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
    "fmt"
    "math/rand"
)

func main() {
    // 种子 = 42,每次运行结果都相同
    // 注意:Go 1.20+ 已废弃 rand.Seed,全局 rand 会自动种子化
    // 如需确定性随机,需使用 rng := rand.New(rand.NewSource(42))
    rng := rand.New(rand.NewSource(42))

    fmt.Println("第一次运行:")
    for i := 0; i < 5; i++ {
        fmt.Printf("  rng.Intn(100) = %d\n", rng.Intn(100))
    }

    fmt.Println("第二次运行(同样的种子):")
    for i := 0; i < 5; i++ {
        fmt.Printf("  rng.Intn(100) = %d\n", rng.Intn(100))
    }
    // 两次输出完全相同!
}

crypto/rand:真随机的"玄学"

crypto/rand 从操作系统获取随机数:

graph TD
    A[硬件噪声源] --> B[Linux: /dev/urandom]
    A --> C[Windows: CryptGenRandom API]
    B --> D[crypto/rand.Reader]
    C --> D
    D --> E[你的密钥]

专业词汇解释:

  • 线性同余生成器(LCG): math/rand 使用的经典算法,公式简单但随机性一般。
  • 梅森旋转(Mersenne Twister): 另一种高质量伪随机算法,周期极长(2^19937-1),被 Python 和 C++ 使用。

9.3 math/rand 快速上手

告别理论,开始实践!rand.Intn(n) 返回 [0, n) 范围内的随机整数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    // 设置当前时间作为种子(真随机起点)
    rand.Seed(time.Now().UnixNano())

    // 生成 0~99 的随机整数
    n := rand.Intn(100)
    fmt.Printf("随机数 [0,100): %d\n", n) // 例如: 42

    // 模拟掷骰子
    dice := rand.Intn(6) + 1
    fmt.Printf("🎲 掷骰子: %d\n", dice) // 1~6

    // 不设种子会怎样?默认种子是 1
    // 所以不调 Seed 的话,每次程序结果都一样(坑!)
    defaultRand := rand.New(rand.NewSource(1)) // 显式用种子1
    fmt.Printf("默认种子 1 的结果: %d\n", defaultRand.Intn(100))
}

警告: Go 1.20 之前,不调用 rand.Seed() 会导致每次运行结果相同!这让无数新手踩坑。从 Go 1.20 开始,math/rand 会自动以全局锁的方式初始化随机种子,妈妈再也不用担心我了。


9.4 Source 与 Rand:随机数生成器的两层结构

这是理解 Go 随机数的关键!

graph TD
    A[Source 底层] --> B[Rand 上层]
    A:---|"Seed + 算法<br/>确定性"|A
    B:---|"便捷方法<br/>线程不安全"|B
    A -->|Int()|B
    B -->|用户调用|B
    A1[rand.Source] -->|接口|A
    A2[rand.NewSource] -->|创建函数|A
    B1[rand.Rand] -->|结构体|B
    B2[rand.New] -->|创建函数|B
  • Source(源): 负责用种子和算法产生随机数序列。它是个接口,定义了 Int63() int64Seed(int64) 方法。
  • Rand(生成器): 负责封装 Source,提供各种便捷方法(IntnFloat64Shuffle 等)。每个 Rand 实例都有状态。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import (
    "fmt"
    "math/rand"
)

func main() {
    // 1. 创建 Source(种子 = 123)
    source := rand.NewSource(123)

    // 2. 用 Source 创建 Rand
    rng := rand.New(source)

    // 3. 调用 Rand 的方法
    fmt.Printf("随机整数: %d\n", rng.Intn(100))    // 0~99
    fmt.Printf("随机浮点: %.4f\n", rng.Float64())  // 0.0~1.0
    fmt.Printf("随机排列: %v\n", rng.Perm(5))      // 0~4 的随机排列
}

专业词汇解释:

  • 线程安全: 默认的 math/rand 全局函数通过全局锁保护 Source,所以是线程安全的,但性能会打折扣。
  • 局部 Rand: 自己 New 的 Rand 实例不共享锁,性能更好,但多 goroutine 共用同一个 Rand 实例会有数据竞争(需要加锁)。

9.5 rand.NewSource:创建自定义随机源

NewSource(seed) 是创建随机源的工厂函数。固定种子 = 固定序列 = 可复现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
    "fmt"
    "math/rand"
)

func main() {
    // 同一个种子,同一个世界
    s1 := rand.NewSource(2024)
    s2 := rand.NewSource(2024)

    r1 := rand.New(s1)
    r2 := rand.New(s2)

    fmt.Println("比较两个同种子的 Rand:")
    for i := 0; i < 3; i++ {
        v1 := r1.Intn(1000)
        v2 := r2.Intn(1000)
        fmt.Printf("  迭代 %d: %d == %d ? %v\n", i+1, v1, v2, v1 == v2)
    }

    // 不同种子,不同命运
    s3 := rand.NewSource(42)
    r3 := rand.New(s3)
    fmt.Printf("\n种子 42 的第一个数: %d\n", r3.Intn(1000))
}

应用场景:

  • 单元测试: 确保"随机"测试用例可复现,bug 不会时而有时而没有
  • 游戏回放: 用相同种子重放一局游戏
  • 科学模拟: 其他研究者可以用相同种子复现你的结果

9.6 rand.New:创建随机数生成器

rand.New(source) 用已有的 Source 创建 Rand 实例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    // 创建一个基于时间种子的 Rand
    rng := rand.New(rand.NewSource(time.Now().UnixNano()))

    // 每次运行都不一样
    fmt.Printf("这次运行: %d\n", rng.Intn(100))
    fmt.Printf("这次运行: %d\n", rng.Intn(100))

    // 也可以从零开始创建 Rand(隐含种子 = 0)
    rng2 := rand.New(rand.NewSource(0))
    fmt.Printf("\n种子 0: %d, %d, %d\n",
        rng2.Intn(100), rng2.Intn(100), rng2.Intn(100))
}

小技巧: 如果你在多个 goroutine 中使用随机数,最好每个 goroutine 创建自己的 Rand 实例(各自不同的种子),避免锁竞争带来的性能问题。


9.7 rand.Int、rand.Int31、rand.Int63:生成随机整数

生成不同位数的随机有符号整数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
    "fmt"
    "math/rand"
    "unsafe"
)

func main() {
    rng := rand.New(rand.NewSource(99))

    // Int() 返回非负随机整数(平台相关,32或64位)
    i := rng.Int()
    fmt.Printf("rand.Int(): %d (平台相关位数)\n", i)

    // Int31() 返回随机 31 位有符号整数
    i31 := rng.Int31()
    fmt.Printf("rand.Int31(): %d (范围: 0 ~ 2,147,483,647)\n", i31)

    // Int63() 返回随机 63 位有符号整数
    i63 := rng.Int63()
    fmt.Printf("rand.Int63(): %d (范围: 0 ~ 9,223,372,036,854,775,807)\n", i63)

    // Int() 可以很大,Int31n/Int63n 则限制范围
    fmt.Printf("\n对比大小:\n")
    fmt.Printf("  Int()   位数: %d\n", unsafe.Sizeof(i)*8)
    fmt.Printf("  Int31() 位数: %d\n", unsafe.Sizeof(i31)*8)
    fmt.Printf("  Int63() 位数: %d\n", unsafe.Sizeof(i63)*8)
}

专业词汇解释:

  • 有符号整数(Signed Integer): 可以表示正数、负数和零。最高位是符号位。
  • 无符号整数(Unsigned Integer): 只能表示正数和零。

9.8 rand.Intn:生成 0~n-1 的随机整数

最常用的方法!rand.Intn(n) 返回 [0, n) 范围的整数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
    "fmt"
    "math/rand"
)

func main() {
    rng := rand.New(rand.NewSource(123))

    // 模拟抽奖:10 个人编号 0~9,中奖者是...
    winner := rng.Intn(10)
    fmt.Printf("🎉 中奖编号: %d\n", winner)

    // 模拟密码:6位数字验证码
    code := 0
    for i := 0; i < 6; i++ {
        digit := rng.Intn(10)
        code = code*10 + digit
    }
    fmt.Printf("📱 短信验证码: %06d\n", code)

    // Intn(0) 会 panic!边界情况要注意
    // rng.Intn(0) // 别这样!
    fmt.Println("注意: Intn(n) 的 n 必须大于 0,否则会 panic!")
}

警告: Intn(0) 会引发 panic!永远不要传 0。


9.9 rand.Int31n、rand.Int63n:带上限的 31/63 位随机整数

底层高效版本,Intn 内部其实就是调用的这些:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
    "fmt"
    "math/rand"
)

func main() {
    rng := rand.New(rand.NewSource(456))

    // Int31n: 随机 31 位整数,再取模
    v31 := rng.Int31n(1000)
    fmt.Printf("rand.Int31n(1000): %d (范围: 0~999)\n", v31)

    // Int63n: 随机 63 位整数,再取模
    v63 := rng.Int63n(10000)
    fmt.Printf("rand.Int63n(10000): %d (范围: 0~9999)\n", v63)

    // 直接用 Int31 然后 %n 也可以,但 Int31n 更均匀
    // 不推荐: rng.Int31() % 100 // 会偏向某些值(如果你不懂原理的话)
    fmt.Println("\n推荐用法:")
    fmt.Println("  rand.Intn(n)   → int 类型,n<1<<31 时使用")
    fmt.Println("  rand.Int31n(n) → int32 类型,精确控制位数")
    fmt.Println("  rand.Int63n(n) → int63 类型,大范围时使用")
}

专业词汇解释:

  • 模偏差(Modulo Bias): 如果你对随机数直接取模,某些值出现的概率会略高。Intn 系列通过算法避免了这个问题。
  • 拒绝采样(Rejection Sampling): Intn 使用的算法:生成比目标范围更大更均匀的随机数,丢弃不合格的,效率更高。

9.10 rand.Float64、rand.Float32:生成 0~1 之间的随机浮点数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
    "fmt"
    "math/rand"
)

func main() {
    rng := rand.New(rand.NewSource(789))

    // Float64: [0.0, 1.0) 范围的 64 位浮点数
    for i := 0; i < 5; i++ {
        f := rng.Float64()
        fmt.Printf("  Float64: %.6f\n", f)
    }

    // Float32: [0.0, 1.0) 范围的 32 位浮点数
    fmt.Println("\nFloat32 (精度更低):")
    for i := 0; i < 3; i++ {
        f := rng.Float32()
        fmt.Printf("  Float32: %.8f\n", f)
    }

    // 实用:生成 [min, max) 范围的浮点数
    min, max := 1.5, 9.5
    r := min + rng.Float64()*(max-min)
    fmt.Printf("\n[%.1f, %.1f) 范围: %.4f\n", min, max, r)
}

注意: 浮点数永远取不到 1.0,最大值约为 0.9999999999999999。这是 IEEE 754 的特性。


9.11 rand.NormFloat64:正态(高斯)分布,均值 0,标准差 1

自然界大多数数据都符合正态分布:身高、考试成绩、测量误差。NormFloat64 生成标准正态分布的随机数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
    "fmt"
    "math/rand"
)

func main() {
    rng := rand.New(rand.NewSource(111))

    fmt.Println("标准正态分布样本(均值=0, 标准差=1):")
    fmt.Println("  大多数值在 -3 ~ +3 之间")
    fmt.Println("  约 68% 在 -1 ~ +1 之间")
    fmt.Println("  约 95% 在 -2 ~ +2 之间")

    sum := 0.0
    samples := []float64{}
    for i := 0; i < 10; i++ {
        v := rng.NormFloat64()
        samples = append(samples, v)
        sum += v
    }
    fmt.Printf("\n  样本值: %+.4f\n", samples)
    fmt.Printf("  样本均值: %.4f (应该接近 0)\n", sum/float64(len(samples)))
}

专业词汇解释:

  • 正态分布(Normal Distribution): 又称高斯分布,钟形曲线,均值处概率最高。
  • 均值(Mean/μ): 分布的中心点。这里 NormFloat64 的均值固定为 0。
  • 标准差(Standard Deviation/σ): 衡量数据分散程度。这里固定为 1。

9.12 rand.ExpFloat64:指数分布,模拟"无记忆"的随机等待时间

指数分布常用于描述独立随机事件发生的时间间隔,比如:

  • 客户到达银行的时间间隔
  • 网站请求的到达间隔
  • 放射性衰变
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
    "fmt"
    "math/rand"
)

func main() {
    rng := rand.New(rand.NewSource(222))

    fmt.Println("指数分布样本(λ=1):")
    fmt.Println("  大多数值较小,少数值很大")
    fmt.Println("  无记忆性:已等待 5 分钟,再等 5 分钟的概率不变")

    for i := 0; i < 10; i++ {
        v := rng.ExpFloat64()
        fmt.Printf("  等待时间: %.4f 单位时间\n", v)
    }

    // 调整 lambda(率参数):值越小,分布越"平缓"
    // Go 的 ExpFloat64 固定 lambda=1
    // 如果需要其他 lambda,可以乘以 1/lambda
    lambda := 0.5
    fmt.Printf("\nlambda=%.1f 的指数分布:\n", lambda)
    for i := 0; i < 5; i++ {
        v := rng.ExpFloat64() / lambda
        fmt.Printf("  %.4f\n", v)
    }
}

专业词汇解释:

  • 指数分布(Exponential Distribution): 描述两次独立事件发生的时间间隔。
  • 无记忆性(Memoryless): 指数分布的独特性质——“已经等了多久"不影响"还要等多久”。

9.13 rand.Perm:生成 0~n-1 的随机排列

Perm(n) 返回一个 [0, 1, 2, ..., n-1] 的随机排列,常用于洗牌算法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
    "fmt"
    "math/rand"
)

func main() {
    rng := rand.New(rand.NewSource(333))

    // 生成一副牌的随机顺序(简化版,4张牌)
    deck := rng.Perm(4)
    fmt.Printf("🃏 洗牌结果: %v\n", deck)
    // 例如 [2, 0, 3, 1]

    // 真实场景:生成测试数据顺序
    n := 10
    order := rng.Perm(n)
    fmt.Printf("\n📋 测试执行顺序: %v\n", order)

    // 抽奖:随机决定 5 个人的出场顺序
    names := []string{"Alice", "Bob", "Charlie", "Diana", "Eve"}
    perm := rng.Perm(5)
    fmt.Println("\n🎤 出场顺序:")
    for i, idx := range perm {
        fmt.Printf("  第 %d 位: %s\n", i+1, names[idx])
    }
}

9.14 rand.Shuffle:打乱已有顺序

Shuffle 直接在原地打乱切片,不返回新切片。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
    "fmt"
    "math/rand"
)

func main() {
    rng := rand.New(rand.NewSource(444))

    // 打乱字符串切片
    colors := []string{"红", "黄", "蓝", "绿", "紫"}
    fmt.Printf("洗牌前: %v\n", colors)

    rng.Shuffle(len(colors), func(i, j int) {
        colors[i], colors[j] = colors[j], colors[i]
    })
    fmt.Printf("洗牌后: %v\n", colors)

    // 打乱整数切片
    numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    rng.Shuffle(len(numbers), func(i, j int) {
        numbers[i], numbers[j] = numbers[j], numbers[i]
    })
    fmt.Printf("随机序列: %v\n", numbers)

    // Shuffle 的 swap 函数签名
    // func(i, j int)
    // i: 第一个位置, j: 第二个位置
    // 交换 slice[i] 和 slice[j]
}

提示: Shuffle 是 Fisher-Yates 洗牌算法的 Go 实现,时间复杂度 O(n)。


9.15 rand.Read:生成随机字节切片

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
    "fmt"
    "math/rand"
)

func main() {
    rng := rand.New(rand.NewSource(555))

    // 生成 16 字节的随机数据
    data := make([]byte, 16)
    n, err := rng.Read(data)
    if err != nil {
        fmt.Printf("错误: %v\n", err)
        return
    }
    fmt.Printf("读取了 %d 字节: %x\n", n, data)

    // 生成 UUID 风格的随机字符串
    uuid := make([]byte, 16)
    rng.Read(uuid)
    fmt.Printf("UUID 风格: %x-%x-%x-%x-%x\n",
        uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:])
}

注意: rand.Readio.Reader 接口的实现,可以直接当数据源用。


9.16 rand.NewZipf:Zipf 分布,模拟"热点数据"

Zipf 分布(齐夫分布)描述了一种"赢家通吃"的现象:

  • 20% 的网站拥有 80% 的访问量
  • 少数词汇占据了大部分语言使用
  • 热门商品被反复购买
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package main

import (
    "fmt"
    "math/big"
    "math/rand"
)

func main() {
    // 创建 Zipf 分布生成器
    // v: 最大值, alpha: 分布参数(>0), q: 指数参数(>=1)
    // alpha 越大,热门越集中
    // q 影响最小值
    source := rand.NewSource(666)
    rng := rand.New(source)

    zipf := rand.NewZipf(rng, 1.5, 1.0, 100000) // alpha=1.5, q=1.0, v=100000

    fmt.Println("Zipf 分布样本(模拟网站访问):")
    fmt.Println("  alpha=1.5 表示热门集中度适中")
    fmt.Println("  v=100000 表示有 10 万个页面")

    // 统计:前 1000 个样本中各"档次"的数量
    bucket := map[string]int{
        "热门(1~10)":      0,
        "中等(11~100)":    0,
        "冷门(101~1000)":  0,
        "长尾(1001~)":     0,
    }

    for i := 0; i < 10000; i++ {
        page := zipf.Uint64()
        switch {
        case page <= 10:
            bucket["热门(1~10)"]++
        case page <= 100:
            bucket["中等(11~100)"]++
        case page <= 1000:
            bucket["冷门(101~1000)"]++
        default:
            bucket["长尾(1001~)"]++
        }
    }

    fmt.Println("\n10000 次访问的分布:")
    for k, v := range bucket {
        pct := float64(v) / 100.0
        bar := ""
        for i := 0; i < int(pct)/2; i++ {
            bar += "█"
        }
        fmt.Printf("  %-12s: %5d 次 (%5.1f%%) %s\n", k, v, pct, bar)
    }
}

专业词汇解释:

  • Zipf 分布: 以语言学家 George Kingsley Zipf 命名,公式:P(k) ∝ 1/k^α
  • 幂律分布(Power Law): Zipf 是幂律分布的一种,特点是"无标度"。

9.17 math/rand/v2(Go 1.20+):新一代随机数 API

Go 1.20 带来了全新的 math/rand/v2!这次升级可不只是版本号+1:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
    "fmt"
    "math.rand/v2"
)

func main() {
    // v2 的核心改进:
    // 1. 默认全局 Rand 无需 Seed,自动初始化
    // 2. 底层算法升级为 PCG(性能更好)
    // 3. 新增 Uint、Uint64 等更直接的 API
    // 4. 分布函数命名更一致(Exp, Norm 等)

    // 快速上手
    fmt.Printf("Uint64: %d\n", v2.Uint64())
    fmt.Printf("IntN(100): %d\n", v2.IntN(100))
    fmt.Printf("Float64: %.6f\n", v2.Float64())

    // 创建自己的 Rand(更推荐的方式)
    rng := v2.New(v2.NewPCG(1234, 5678))

    fmt.Printf("\n自定义 Rand (PCG):\n")
    for i := 0; i < 5; i++ {
        fmt.Printf("  %d\n", rng.IntN(1000))
    }
}

升级亮点:

  • PCG 算法: 比旧版 LCG 随机性更好,性能相当
  • 无需 Seed: 全局函数开箱即用,不会踩"种子=1"的坑
  • 命名统一: UintUint64IntN——和 crypto/rand 更一致

9.18 crypto/rand.Int:加密安全的随机大整数

密码学应用需要"不可预测"的随机数。crypto/rand.Int 生成加密安全的大整数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
    "crypto/rand"
    "fmt"
    "math/big"
)

func main() {
    // 生成 [0, n) 范围内的加密安全随机整数
    n, _ := new(big.Int).SetString("100000000000000000000", 10) // 10^20
    k, err := rand.Int(rand.Reader, n)
    if err != nil {
        fmt.Printf("错误: %v\n", err)
        return
    }
    fmt.Printf("加密安全随机数 [0, 10^20): %d\n", k)

    // 生成大素数(RSA 密钥需要)
    // 实际中用 crypto/rand 读取足够多的字节,再找素数
    prime, err := rand.Prime(rand.Reader, 256) // 256 位的素数
    if err != nil {
        fmt.Printf("错误: %v\n", err)
        return
    }
    fmt.Printf("256位素数: %x...\n", prime.Text(16)[:32])
}

注意: crypto/rand.Int 需要 math/bigInt 类型作为参数,返回值也是 big.Int。对于普通 int 范围,用下面的 Read 更直接。


9.19 crypto/rand.Reader:读取加密随机字节

crypto/rand.Reader 是一个全局的 io.Reader,从操作系统获取高质量熵。

graph LR
    A[硬件噪声] --> B[OS 熵源]
    A --> C[系统调用]
    B --> D[/dev/urandom<br/>CryptGenRandom]
    C --> D
    D --> E[crypto/rand.Reader]
    E --> F[密钥生成<br/>Nonce 初始化<br/>盐值]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
    "crypto/rand"
    "fmt"
)

func main() {
    // 生成 32 字节的随机数(256 位)
    key := make([]byte, 32)
    n, err := rand.Read(key)
    if err != nil {
        fmt.Printf("熵源错误: %v\n", err)
        return
    }
    fmt.Printf("生成了 %d 字节的加密随机数:\n", n)
    fmt.Printf("  %x\n", key)

    // 用于密码学场景
    fmt.Printf("\n使用场景:\n")
    fmt.Printf("  - AES 密钥: %d 字节\n", 32)
    fmt.Printf("  - HMAC 密钥: %d 字节\n", 32)
    fmt.Printf("  - RSA 签名 nonce: 至少 %d 字节\n", 32)
}

专业词汇解释:

  • Nonce: 只能使用一次的随机数,用于加密协议中防止重放攻击。
  • Salt: 随机盐值,和密码一起哈希存储,防止彩虹表攻击。

9.20 crypto/rand.Read:生成加密随机字节

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
    "crypto/rand"
    "fmt"
)

func main() {
    // 生成指定长度的加密随机字节
    token := make([]byte, 16)
    _, err := rand.Read(token)
    if err != nil {
        fmt.Printf("错误: %v\n", err)
        return
    }
    fmt.Printf("会话 Token: %x\n", token)

    // 生成验证码
    digits := make([]byte, 6)
    rand.Read(digits)
    for i := range digits {
        digits[i] = digits[i] % 10 // 只取数字 0~9
    }
    fmt.Printf("数字验证码: %s\n", digits)
}

重要: crypto/rand 的操作通常比 math/rand 慢几个数量级。对于不需要密码学安全的场景(如游戏、模拟),请使用 math/rand


9.21 crypto/rand vs math/rand:密码/token/密钥用 crypto,游戏骰子用 math

最后,来一张对比图,让你选对工具:

graph TD
    A[我需要随机数!] --> B{用途是什么?}
    B -->|🔐 密钥/密码/Token| C[使用 crypto/rand]
    B -->|🎲 游戏骰子| D[使用 math/rand]
    B -->|📊 模拟/统计| D
    B -->|🧪 测试需要复现| D

    C --> C1[安全,但慢]
    D --> D1[快,可复现]

    C1 --> E1[密钥生成: crypto/rand.Int]
    C1 --> E2[随机字节: crypto/rand.Read]
    D1 --> F1[IntN: rand.Intn]
    D1 --> F2[分布: NormFloat64, ExpFloat64]
    D1 --> F3[洗牌: rand.Shuffle]
场景推荐原因
密码学密钥crypto/rand不可预测,攻击者无法猜测
Session Tokencrypto/rand安全第一
游戏骰子math/rand速度快,可复现调试
蒙特卡洛模拟math/rand需要大量随机数
单元测试math/rand + 固定种子每次运行结果相同
机器学习math/rand速度重要
洗牌/抽奖math/rand不涉及安全
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
    "crypto/rand"
    "fmt"
    "math/rand"
)

func main() {
    fmt.Println("=== 选择指南 ===")
    fmt.Println("✅ crypto/rand 使用场景:")
    fmt.Println("   - 生成密码")
    fmt.Println("   - 生成 API Token")
    fmt.Println("   - 生成 SSH 密钥")
    fmt.Println("   - 加密盐值")

    fmt.Println("\n✅ math/rand 使用场景:")
    fmt.Println("   - 游戏随机事件")
    fmt.Println("   - 数据采样/打乱")
    fmt.Println("   - 蒙特卡洛模拟")
    fmt.Println("   - 需要复现的测试")

    // 示例:生成安全的随机密码
    charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    password := make([]byte, 16)
    crypto_rand.Read(password) // 加密安全
    for i := range password {
        password[i] = charset[int(password[i])%len(charset)]
    }
    fmt.Printf("\n🔐 随机密码: %s\n", password)
}

本章小结

概念要点
math/rand伪随机数,速度快,可复现,适合游戏/模拟
crypto/rand真随机数,密码学安全,慢,适合密钥/Token
Source & RandSource 管种子和算法,Rand 管用户接口
rand.NewSource固定种子 = 固定序列 = 可复现
rand.Intn(n)返回 [0, n),最常用的方法
rand.Float64返回 [0.0, 1.0),用于缩放
rand.NormFloat64标准正态分布,均值0,标准差1
rand.ExpFloat64指数分布,无记忆性
rand.Perm / Shuffle洗牌和随机排列
rand.NewZipfZipf分布,模拟热点数据
math/rand/v2Go 1.20+ 新API,自动种子,PCG算法
crypto/rand.ReaderOS 熵源的 io.Reader 接口
crypto/rand.Int加密安全的大整数
选择原则安全场景用 crypto,游戏模拟用 math

记住: 计算机里的"随机"都是伪装的确定性。选择正确的随机数源,让你的程序既高效又安全!

最后修改 March 30, 2026: 新增 Go 标准库基础 教程 (acbc3f6)