30 - Go 随机数与 UUID 生成:原理、陷阱与工程实践

文章目录


30 - Go 随机数与 UUID 生成:原理、陷阱与工程实践

在日常 Go 开发中,我们几乎一定会遇到这两类需求:

  • 生成随机数(验证码、抽奖、负载均衡、测试数据)
  • 生成唯一 ID(订单号、请求链路 ID、数据库主键)

很多人会觉得:

"随机数不就是 rand.Intn() 吗?UUID 不就是调库生成一下?"

但实际上:

  • 随机数有"伪随机"和"真随机"
  • UUID 有不同版本
  • 错误使用随机种子可能导致严重线上事故
  • UUID 在数据库中的性能可能非常差
  • 并发环境下的随机源设计非常关键

这篇文章,我们不仅讲"怎么用",更讲:

Go 为什么这样设计?背后的本质是什么?


核心概念

随机数到底解决什么问题?

随机数的核心目标:

在"不确定性"中生成可用的数据。

常见场景:

场景 示例
安全领域 Token、密码、JWT Secret
业务领域 验证码、抽奖
系统领域 负载均衡、随机退避
测试领域 Mock 数据

但很多开发者忽略一个问题:

"随机"其实有不同等级。


Go 中的随机数本质

Go 里主要有两套随机系统:

类型 用途
math/rand 伪随机 普通业务
crypto/rand 真随机(密码学安全) 安全场景

它们最大的区别:

对比项 math/rand crypto/rand
是否可预测 可以 很难预测
性能 较慢
是否安全 不安全 安全
是否依赖种子

小结

随机数并不是真的"随机"。

很多随机算法,本质上是:

"根据一个初始状态不断推导下一个值。"

这个初始状态,就是 Seed(种子)。


UUID 又是什么?

UUID(Universally Unique Identifier):

全球唯一标识符。

典型格式:

text 复制代码
550e8400-e29b-41d4-a716-446655440000

UUID 的目标:

  • 不依赖数据库自增
  • 分布式唯一
  • 不依赖中心节点

这对于微服务、分布式系统非常重要。


基础使用示例

注意:

在 Go1.20 之前,

math/rand 需要手动调用:

rand.Seed(time.Now().UnixNano())

否则每次程序启动生成的随机序列都相同。

而从 Go1.20 开始,

Go 已默认自动初始化随机种子,

因此即使不手动 Seed,

随机结果也会不同。

不过在工程实践中,

仍推荐使用 rand.New + rand.NewSource

创建独立随机源,

避免污染全局随机状态。

使用 math/rand 生成随机数

go 复制代码
package main

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

func main() {
	// 使用当前时间作为随机种子
	rand.Seed(time.Now().UnixNano())

	// 生成 0~99 的随机数
	number := rand.Intn(100)

	fmt.Println(number)
}

你可以理解成:

text 复制代码
当前时间
   ↓
作为 seed
   ↓
rand 根据 seed 推导
   ↓
生成随机数

为什么必须 Seed?

如果你不设置种子:

go 复制代码
package main

import (
	"fmt"
	"math/rand"
)

func main() {
	fmt.Println(rand.Intn(100))
}

你会发现(概率性重复):

text 复制代码
81
87
47
...

每次程序启动结果都一样。

因为默认 Seed 是固定值。


小结

text 复制代码
math/rand = 用 seed 推导随机序列

而:

go 复制代码
rand.Seed(time.Now().UnixNano())

作用就是:

text 复制代码
让每次程序运行时的 seed 都不同

这样随机结果才不同。


使用 UUID

Go 中最常见的是:

  • github.com/google/uuid
bath 复制代码
# 初始化 Go Module
go mod init demo

# 下载 uuid 依赖
go get github.com/google/uuid

# 运行程序
go run main.go

示例:

go 复制代码
package main

import (
	"fmt"

	"github.com/google/uuid"
)

func main() {
	id := uuid.New()

	fmt.Println(id.String())
}

输出:

text 复制代码
3f4f3f1c-cdb5-4d84-9b58-67b0d4e2c1b7

进阶使用示例

使用 crypto/rand 生成安全 Token

很多人错误地用 math/rand 生成登录 Token:

go 复制代码
token := fmt.Sprintf("%d", rand.Int())

这是极其危险的。

正确做法:

go 复制代码
package main

import (
	"crypto/rand"
	"encoding/hex"
	"fmt"
)

func main() {
	// 生成随机字符串
	buffer := make([]byte, 16)  // 16字节的随机字符串
	_, err := rand.Read(buffer) // 填充随机数据
	if err != nil {
		panic(err)
	}
	// 将随机字节转换为16进制字符串
	token := hex.EncodeToString(buffer)
	fmt.Println(token)
}

输出:

text 复制代码
a215d4d030547da49fbba73fa1d71dfb

为什么安全?

因为:

  • 数据来自操作系统随机源
  • Linux 通常来自 /dev/urandom
  • 无法通过 seed 推导

这才是真正意义上的"不可预测"。


生成指定范围随机字符串

很多业务都需要:

  • 随机验证码
  • 邀请码
  • 短链 Key

示例:

go 复制代码
package main

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

// 生成一个随机字符串
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" // 定义字符集

// 生成一个随机字符串
func generateRandomString(length int) string {
	rand.Seed(time.Now().UnixNano()) // 初始化随机数生成器,确保每次运行结果不同

	result := make([]byte, length) // 创建一个长度为length的byte切片

	// 填充切片随机选择字符
	for i := range result {
		result[i] = letters[rand.Intn(len(letters))]
	}

	return string(result)
}
func main() {
	fmt.Println(generateRandomString(8)) // 生成一个长度为8的随机字符串
}

这里其实有坑

这个代码虽然能运行:

但每次调用都重新 Seed。

高并发下可能生成重复字符串。

核心问题是:

  • 每次函数调用都 Seed
  • 高并发下 time.Now().UnixNano() 可能相同
  • 导致随机序列"重置"
  • 最终可能生成重复字符串

本质上是:

❗ 每次都在"从同一个起点重新随机"

解决办法:

独立随机源(更工程化)

go 复制代码
var r = rand.New(
	rand.NewSource(time.Now().UnixNano()),
)

func generateRandomString(length int) string {
	result := make([]byte, length)

	for i := range result {
		result[i] = letters[r.Intn(len(letters))]
	}

	return string(result)
}

一句话点睛

❗ 随机数真正的坑,不是"不够随机",而是"不断重置随机起点"。


基于 UUID 的订单号设计

很多系统直接使用 UUID 作为订单号:

text 复制代码
550e8400-e29b-41d4-a716-446655440000

问题:

  • 太长
  • 不可读
  • 数据库索引性能差

更合理的做法:

text 复制代码
时间戳 + 随机数

示例:

go 复制代码
package main

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

// 创建独立随机源
var r = rand.New(
	rand.NewSource(time.Now().UnixNano()), // 使用当前纳秒时间戳作为种子
)

// 生成订单ID的函数
func generateOrderID() string {
	now := time.Now().Unix() // 获取当前时间戳(秒级)

	randomPart := r.Intn(100000) // 生成一个0到99999之间的随机数

	return fmt.Sprintf("%d%05d", now, randomPart) // 将时间戳和随机数格式化为字符串,随机数部分补零到5位
}

func main() {
	orderID := generateOrderID()
	fmt.Println("生成的订单ID:", orderID)
}

小结

UUID 的优势:

  • 唯一性强
  • 分布式友好

UUID 的缺点:

  • 长度大
  • 索引离散
  • 不适合聚簇索引

所以:

"唯一"并不代表"适合数据库"。


常见错误与坑(重点)

坑一:安全场景使用 math/rand


错误代码

go 复制代码
resetToken := fmt.Sprintf("%d", rand.Int())

为什么危险?

攻击者只要知道:

  • 随机算法
  • Seed 范围

就能推测生成结果。

这在:

  • 密码重置
  • Session Token
  • 验证码

场景中是严重漏洞。


正确写法

go 复制代码
package main

import (
	cryptoRand "crypto/rand" // crypto/rand 更安全
	"encoding/hex"
	"fmt"
)

func main() {
	buffer := make([]byte, 32) // 创建一个长度为32的byte数组

	_, err := cryptoRand.Read(buffer) // 生成随机数
	if err != nil {
		panic(err)
	}
	resetToken := hex.EncodeToString(buffer) // 将byte数组转换为16进制字符串
	fmt.Println(resetToken)                  // 打印结果
}

思考点

为什么密码学随机数更慢?

因为:

  • 需要系统熵池
  • 需要不可预测
  • 需要抵抗推导攻击

它追求的是"安全",而不是"性能"。


坑二:UUID 作为 MySQL 主键


错误设计

sql 复制代码
id CHAR(36) PRIMARY KEY

为什么性能差?

UUID v4 完全随机。

会导致:

  • B+Tree 插入离散
  • 页分裂频繁
  • 索引碎片严重

数据库性能会越来越差。


更合理方案

可以使用:

  • Snowflake
  • UUID v7
  • 时间有序 ID

小结

数据库最喜欢:

text 复制代码
递增 ID

数据库最讨厌:

text 复制代码
完全随机 ID

底层原理解析(核心)

math/rand 的本质

Go 的 math/rand

本质是:

伪随机数生成器(PRNG)

核心逻辑:

text 复制代码
next = f(previous)

即:

text 复制代码
下一个随机数 = 当前状态经过算法计算

crypto/rand 的本质

crypto/rand

不自己实现随机算法。

它依赖:

  • Linux 熵池
  • 内核随机设备
  • CPU 随机指令

本质:

从操作系统获取不可预测的数据。


Linux 熵池

系统会收集:

  • 鼠标移动
  • 网络抖动
  • 磁盘 IO
  • CPU 时间差

形成 entropy(熵)。

然后生成随机数据。


思考点

为什么随机数和"熵"有关?

因为:

随机的本质,是"不确定性"。

熵越高:

  • 越不可预测
  • 越安全

UUID 的底层结构

UUID v4:

text 复制代码
122 bit 随机数 + 版本信息

格式:

text 复制代码
xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx

其中:

  • 4 表示 v4
  • y 表示变体位

为什么 UUID 能全球唯一?

因为:

text 复制代码
2^122

空间极其巨大。

碰撞概率低到几乎可以忽略。


对比与扩展

math/rand vs crypto/rand

对比 math/rand crypto/rand
类型 伪随机 真随机
性能
是否安全
是否可预测
是否需要 Seed

UUID v1 vs v4 vs v7

版本 特点 问题
v1 时间 + MAC 地址 泄露机器信息
v4 完全随机 数据库性能差
v7 时间有序 更适合数据库

为什么 UUID v7 越来越流行?

因为它兼顾:

  • 唯一性
  • 时间有序
  • 数据库友好

这是现代分布式系统的重要趋势。


最佳实践

普通业务随机数

直接使用:

go 复制代码
math/rand

适合:

  • 抽奖
  • 随机展示
  • 测试数据

安全场景

必须使用:

go 复制代码
crypto/rand

包括:

  • Token
  • 密码
  • 验证码
  • 密钥

Seed 只初始化一次

推荐:

go 复制代码
func init() {
	rand.Seed(time.Now().UnixNano())
}

不要:

  • 重复 Seed
  • 并发 Seed

UUID 不要无脑做主键

优先考虑:

  • Snowflake
  • UUID v7
  • 自增 ID

尤其 MySQL InnoDB。


封装统一随机组件

工程中建议:

text 复制代码
random/
    math.go
    crypto.go
    uuid.go

统一:

  • 随机策略
  • Token 生成
  • UUID 管理

避免团队乱用。


思考与升华

很多开发者理解随机数:

text 复制代码
随机 = 不可预测

但计算机本质是:

text 复制代码
确定性机器

它实际上很难真正随机。

所以:

  • math/rand 是"算法模拟随机"
  • crypto/rand 是"利用现实世界的不确定性"

这就是两者设计哲学的根本区别。


一个简化版 PRNG 实现

go 复制代码
package main

import "fmt"

type MyRand struct {
	seed int
}

func (r *MyRand) Next() int {
	r.seed = (r.seed*1103515245 + 12345) & 0x7fffffff
	return r.seed
}

func main() {
	r := MyRand{seed: 1}

	for i := 0; i < 5; i++ {
		fmt.Println(r.Next())
	}
}

你会发现:

  • 结果"看起来随机"
  • 但其实完全确定

这就是伪随机的本质。


点睛总结

随机数与 UUID 的核心,不是"生成一个值"。

而是:

在"唯一性"、"性能"、"安全性"、"可预测性"之间做权衡。

真正成熟的 Go 开发者:

不会只会调用 API。

而会思考:

  • 为什么要这样设计?
  • 为什么安全随机更慢?
  • 为什么 UUID 会影响数据库?
  • 为什么伪随机依赖 Seed?

因为:

工程世界里,所有"随机"的背后,其实都是"设计"。

相关推荐
xiaoye-duck3 小时前
Qt 初识核心:从 HelloWorld 到基础控件,吃透对象树与内存管理
开发语言·qt
我的xiaodoujiao3 小时前
API 接口自动化测试详细图文教程学习系列19--添加封装其他的方法
开发语言·python·学习·测试工具·pytest
Co_Hui3 小时前
Java: 集合
java·开发语言
ch.ju3 小时前
Java程序设计(第3版)第四章——动态部分
java·开发语言
诙_3 小时前
C++学习总结
开发语言·c++·学习
2401_865439633 小时前
探索JavaScript对象创建的灵活方式
开发语言·javascript·ecmascript
程序猿~厾罗3 小时前
回归更新,一个简单的重新认识
开发语言
我命由我123453 小时前
Android 开发:Unable to start service Intent { ... } U=0: not found
android·开发语言·android studio·android jetpack·android-studio·android runtime
三品吉他手会点灯3 小时前
C语言学习笔记 - 34.数据类型 - 编程规范与高效学习方法
c语言·开发语言·笔记·学习