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

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

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

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

很多人会觉得:

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

但实际上:

  • 随机数有“伪随机”和“真随机”
  • UUID 有不同版本
  • 错误使用随机种子可能导致严重线上事故
  • 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 中最常见的是:

  • github.com/google/uuid
# 初始化 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

为什么安全?

因为:

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

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

生成指定范围随机字符串

很多业务都需要:

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

示例:

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() 可能相同
  • 导致随机序列“重置”
  • 最终可能生成重复字符串

本质上是:

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

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

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

为什么危险?

攻击者只要知道:

  • 随机算法
  • Seed 范围

就能推测生成结果。

这在:

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

场景中是严重漏洞。

正确写法

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 完全随机。

会导致:

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

数据库性能会越来越差。

更合理方案

可以使用:

  • Snowflake
  • UUID v7
  • 时间有序 ID

小结

数据库最喜欢:

递增 ID

数据库最讨厌:

完全随机 ID

底层原理解析(核心)

math/rand 的本质

Go 的 math/rand

本质是:

伪随机数生成器(PRNG)

核心逻辑:

next = f(previous)

即:

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

crypto/rand 的本质

crypto/rand

不自己实现随机算法。

它依赖:

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

本质:

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

Linux 熵池

系统会收集:

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

形成 entropy(熵)。

然后生成随机数据。

思考点

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

因为:

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

熵越高:

  • 越不可预测
  • 越安全

UUID 的底层结构

UUID v4:

122 bit 随机数 + 版本信息

格式:

xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx

其中:

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

为什么 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

包括:

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

Seed 只初始化一次

推荐:

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

不要:

  • 重复 Seed
  • 并发 Seed

UUID 不要无脑做主键

优先考虑:

  • Snowflake
  • UUID v7
  • 自增 ID

尤其 MySQL InnoDB。

封装统一随机组件

工程中建议:

random/
    math.go
    crypto.go
    uuid.go

统一:

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

避免团队乱用。

思考与升华

很多开发者理解随机数:

随机 = 不可预测

但计算机本质是:

确定性机器

它实际上很难真正随机。

所以:

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

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

一个简化版 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。

而会思考:

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

因为:

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

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

相关文章

  • Go 并发读写 sync.map 详细

    Go 并发读写 sync.map 详细

    阅读本文你将会明确 sync.Map 和原生 map +互斥锁/读写锁之间的性能情况。标准库 sync.Map 虽说支持并发读写 map,但更适用于读多写少的场景,因为他写入的性能比较差,使用时要考虑清楚这一点。
    2021-10-10
  • Go并发同步Mutex典型易错使用场景

    Go并发同步Mutex典型易错使用场景

    这篇文章主要为大家介绍了Go并发同步Mutex典型易错使用场景示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08
  • Go语言七篇入门教程四通道及Goroutine

    Go语言七篇入门教程四通道及Goroutine

    这篇文章主要为大家介绍了Go语言的通道及Goroutine示例详解,本文是Go语言七篇入门系列篇,有需要的朋友可以借鉴参考下,希望能够有所帮助
    2021-11-11
  • go解析svn log生成的xml格式的文件

    go解析svn log生成的xml格式的文件

    这篇文章主要介绍了go解析svn log生成的xml格式的文件的方法,非常的实用,有需要的小伙伴可以参考下。
    2015-04-04
  • golang函数的返回值实现

    golang函数的返回值实现

    本文主要介绍了golang函数的返回值实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-03-03
  • Golang使用原生http实现中间件的代码详解

    Golang使用原生http实现中间件的代码详解

    中间件(middleware):常被用来做认证校验、审计等,家常用的Iris、Gin等web框架,都包含了中间件逻辑,但有时我们引入该框架显得较为繁重,本文将介绍通过golang原生http来实现中间件操作,需要的朋友可以参考下
    2024-05-05
  • 简单高效!Go语言封装二级认证功能实现

    简单高效!Go语言封装二级认证功能实现

    本文将介绍如何使用Go语言封装二级认证功能,实现简单高效的用户认证流程,二级认证是一种安全措施,要求用户在登录后进行额外的身份验证,以提高账户安全性,
    2023-10-10
  • golang实现循环队列的示例代码

    golang实现循环队列的示例代码

    循环队列是一种使用固定大小的数组来实现队列的数据结构,本文主要介绍了golang实现循环队列的示例代码,具有一定的参考价值,感兴趣的可以了解一下
    2024-07-07
  • 手把手带你运行自己的第一个Go程序

    手把手带你运行自己的第一个Go程序

    Go语言被设计成一门应用于搭载Web服务器,存储集群或类似用途的巨型中央服务器的系统编程语言,这篇文章主要介绍了如何运行自己的第一个Go程序的相关资料,需要的朋友可以参考下
    2025-07-07
  • golang实现简易的分布式系统方法

    golang实现简易的分布式系统方法

    这篇文章主要介绍了golang实现简易的分布式系统方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-10-10

最新评论