登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go math/rand 全局源并发使用有哪些误区

来源:17golang原创

时间:2026-11-14 16:18:04 153浏览 收藏

Go 的 math/rand 并发误区,通常不是“随机数会不会重复”,而是把不同层级的对象当成了同一种东西。结论先说清:rand.Intn、rand.Float64 这类顶层函数可以被多个 goroutine 并发调用;通过 rand.NewSource 创建的 Source,以及由它创建的 *rand.Rand,默认不能被多个 goroutine 直接共享。共享自建实例时要么给每个 goroutine 独占,要么在外层加同步。

官方参考:https://pkg.go.dev/math/rand

先分清顶层函数、Rand 与 Source

math/rand 可以拆成三层来理解:顶层函数使用包内部的默认源;rand.New(src) 把一个 Source 包装成 *Rand;rand.NewSource(seed) 返回一个可复现、但不保证并发安全的源。真正容易出错的地方,是看到“全局”二字就以为所有随机数对象都带锁。

Go math/rand 顶层函数、默认源、Rand 和 Source 的并发边界结构图
图1:并发边界结构说明图,区分顶层函数使用的默认源与自建 Rand、Source 实例。
调用对象默认并发边界工程选择
rand.Intn 等顶层函数可被多个 goroutine 并发使用简单的非安全随机任务优先直接调用
rand.NewSource 返回的 Source不支持直接并发共享独占,或由同步层保护
*rand.Rand方法不能与其他 Rand 方法并发调用每个 worker 一个,或封装互斥锁
crypto/rand面向安全随机数的另一套 API令牌、密钥等场景不要用 math/rand

全局函数为什么可以直接放进 goroutine

顶层函数和自建实例的并发语义不同。下面的代码只依赖包级入口,多个 goroutine 可以同时取一个范围内的整数;它没有把 rand.NewSource 返回值塞进共享结构,也没有暴露一个可变的 *rand.Rand 给所有 worker。

package main

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

func main() {
    var wg sync.WaitGroup
    for worker := 0; worker 

“可并发使用”不等于“每次结果都不同”,也不等于“结果不可预测”。math/rand 仍然是伪随机数包;如果业务需要密码学意义上的随机性,应改用 crypto/rand。另外,rand.Intn(n) 要求 n > 0,把外部配置直接传进去仍可能因为零值或负值触发 panic。

自建 Rand 要么独占,要么加锁

需要固定种子、独立随机序列或可复现实验时,通常会写 rand.New(rand.NewSource(seed))。这一步创建的是应用自己的状态,不会自动继承顶层函数的并发保证。最简单的做法是把它放在 worker 内部,让每个 worker 拥有自己的实例。

func worker(seed int64, count int) []int {
    // 每个 worker 独占 Source 和 Rand,避免多个 goroutine 读写同一状态。
    localRand := rand.New(rand.NewSource(seed))
    values := make([]int, 0, count)
    for i := 0; i 

如果业务必须共享一个序列,就把锁和 *rand.Rand 绑定在同一个小对象里,避免调用方绕过锁直接拿到内部指针。锁保护的是整个 Rand 方法调用,不是只保护种子字段。

type SafeRand struct {
    mu sync.Mutex
    r  *rand.Rand
}

func (s *SafeRand) Intn(n int) int {
    s.mu.Lock()
    defer s.mu.Unlock()
    // 锁覆盖完整的 Rand 调用,防止内部状态被并发修改。
    return s.r.Intn(n)
}

func newSafeRand(seed int64) *SafeRand {
    // 只在构造阶段创建实例,后续统一通过 SafeRand.Intn 访问。
    return &SafeRand{r: rand.New(rand.NewSource(seed))}
}
Go 自建 Rand 的独占实例与互斥锁共享结构说明图
图2:自建 Rand 的所有权结构示意图,对比 worker 独占与 SafeRand 统一加锁两种边界。

Seed、伪随机和参数边界不要混在一起

还有几个容易被并发问题掩盖的误区:

  • 不要为了“随机”在启动时反复调用顶层 Seed。当前文档已将包级 Seed 标记为 deprecated;如果目标是可复现序列,应显式创建带种子的独立 Rand,而不是修改包级默认状态。
  • 不要把伪随机当作安全随机。抽样、洗牌、模拟可以考虑 math/rand;验证码、重置令牌、密钥和签名材料应使用 crypto/rand。
  • 不要只看 race detector 的结果来决定设计。没有数据竞争,只能说明当前访问没有被检测到不安全并发,不能把“共享可变序列”自动变成合适的架构。先决定需要独立序列还是统一序列,再选独占或加锁。
  • 不要忽略边界参数。Intn(0) 和负数上界都会 panic;配置值、数组长度和业务过滤结果都应在调用前确认大于零。
速记:顶层函数可以并发用;自建 Source 和 Rand 默认按单 goroutine 设计。要复现就独占实例,要共享就用锁;要安全随机就换 crypto/rand。

相关问题

把 rand.Rand 放进单例后,所有方法都能并发调用吗?

不能。单例只是生命周期设计,不会自动增加同步;应让每个调用方独占实例,或通过一个不暴露内部指针的加锁包装访问。

顶层 rand.Intn 和 rand.New 的 Rand 有什么本质区别?

前者走包级默认源,并发语义由标准库保证;后者由调用方持有状态,种子和序列可控,但并发责任也回到了调用方。

固定种子是不是就能保证多个 goroutine 的结果顺序?

固定种子只能让同一个状态按同一访问顺序产生可复现序列。多个 goroutine 的调度顺序不固定,因此不能仅凭固定种子保证跨运行的调用顺序。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>