Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Go随机数与UUID生成

Go随机数与UUID生成原理与避坑指南

作者:XMYX-0

本文介绍了Go中的随机数生成和UUID生成的相关知识,包括两种随机数生成库的区别和使用场景,UUID的版本和优缺点,以及在实际工程中如何正确使用这两种工具的建议,文章强调了在实际应用中,开发者需要理解背后的原理和设计理念,并做出合适的选择,需要的朋友可以参考下

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

很多人会觉得:

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

但实际上:

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

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

核心概念

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

随机数的核心目标:

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

常见场景:

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

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

“随机”其实有不同等级。

Go 中的随机数本质

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

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

它们最大的区别:

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

小结

随机数并不是真的“随机”。

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

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

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

UUID 又是什么?

UUID(Universally Unique Identifier):

全球唯一标识符。

典型格式:

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 生成随机数

package main

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

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

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

	fmt.Println(number)
}

你可以理解成:

当前时间
   ↓
作为 seed
   ↓
rand 根据 seed 推导
   ↓
生成随机数

为什么必须 Seed?

如果你不设置种子:

package main

import (
	"fmt"
	"math/rand"
)

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

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

81
87
47
...

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

因为默认 Seed 是固定值。

小结

math/rand = 用 seed 推导随机序列

而:

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

作用就是:

让每次程序运行时的 seed 都不同

这样随机结果才不同。

使用 UUID

Go 中最常见的是:

# 初始化 Go Module
go mod init demo
# 下载 uuid 依赖
go get github.com/google/uuid
# 运行程序
go run main.go

示例:

package main

import (
	"fmt"

	"github.com/google/uuid"
)

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

	fmt.Println(id.String())
}

输出:

3f4f3f1c-cdb5-4d84-9b58-67b0d4e2c1b7

进阶使用示例

使用 crypto/rand 生成安全 Token

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

token := fmt.Sprintf("%d", rand.Int())

这是极其危险的。

正确做法:

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)
}

输出:

a215d4d030547da49fbba73fa1d71dfb

为什么安全?

因为:

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

生成指定范围随机字符串

很多业务都需要:

示例:

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。

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

核心问题是:

本质上是:

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

解决办法:
独立随机源(更工程化)

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 作为订单号:

550e8400-e29b-41d4-a716-446655440000

问题:

更合理的做法:

时间戳 + 随机数

示例:

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

错误代码

resetToken := fmt.Sprintf("%d", rand.Int())

为什么危险?

攻击者只要知道:

就能推测生成结果。

这在:

场景中是严重漏洞。

正确写法

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 主键

错误设计

id CHAR(36) PRIMARY KEY

为什么性能差?

UUID v4 完全随机。

会导致:

数据库性能会越来越差。

更合理方案

可以使用:

小结

数据库最喜欢:

递增 ID

数据库最讨厌:

完全随机 ID

底层原理解析(核心)

math/rand 的本质

Go 的 math/rand

本质是:

伪随机数生成器(PRNG)

核心逻辑:

next = f(previous)

即:

下一个随机数 = 当前状态经过算法计算

crypto/rand 的本质

crypto/rand

不自己实现随机算法。

它依赖:

本质:

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

Linux 熵池

系统会收集:

形成 entropy(熵)。

然后生成随机数据。

思考点

为什么随机数和“熵”有关?

因为:

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

熵越高:

UUID 的底层结构

UUID v4:

122 bit 随机数 + 版本信息

格式:

xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx

其中:

为什么 UUID 能全球唯一?

因为:

2^122

空间极其巨大。

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

对比与扩展

math/rand vs crypto/rand

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

UUID v1 vs v4 vs v7

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

为什么 UUID v7 越来越流行?

因为它兼顾:

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

最佳实践

普通业务随机数

直接使用:

math/rand

适合:

安全场景

必须使用:

crypto/rand

包括:

Seed 只初始化一次

推荐:

func init() {
	rand.Seed(time.Now().UnixNano())
}

不要:

UUID 不要无脑做主键

优先考虑:

尤其 MySQL InnoDB。

封装统一随机组件

工程中建议:

random/
    math.go
    crypto.go
    uuid.go

统一:

避免团队乱用。

思考与升华

很多开发者理解随机数:

随机 = 不可预测

但计算机本质是:

确定性机器

它实际上很难真正随机。

所以:

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

一个简化版 PRNG 实现

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。

而会思考:

因为:

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

以上就是Go随机数与UUID生成原理与避坑指南的详细内容,更多关于Go随机数与UUID生成的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文