
本文介绍在 Go 中实现“从 1 到 6 中逐个随机选取、不重复直至遍历完毕,再重新洗牌”的可靠方案,避免传统 rand.Intn 导致的连续重复问题,并解决跨程序运行状态保持的关键难点。
本文介绍在 go 中实现“从 1 到 6 中逐个随机选取、不重复直至遍历完毕,再重新洗牌”的可靠方案,避免传统 `rand.intn` 导致的连续重复问题,并解决跨程序运行状态保持的关键难点。
在日常开发中,尤其是调度类逻辑(如每日菜谱轮换、任务分发、测试用例顺序控制),我们常需要「伪随机但保证全覆盖」的行为:即在有限集合(如数字 1–6)中每次取一个值,绝不重复,直到所有值都被选过一次后,再整体打乱重来。这与单纯调用 rand.Intn(6)+1 有本质区别——后者是真正独立同分布的随机,天然允许重复(例如连续三次抽到 3),无法满足“确保每种选项公平轮到”的业务目标。
✅ 正确做法:使用 rand.Perm() 构建可复用的轮询序列
Go 标准库提供的 rand.Perm(n) 是专为此类场景设计的工具:它返回一个长度为 n 的、包含 0 到 n−1 各一次的随机排列切片。只需对结果整体加 1,即可获得 1–6 的无重复随机序列:
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
// 初始化全局随机源(仅需一次,避免重复 Seed)
rand.Seed(time.Now().UnixNano())
// 生成 1–6 的随机排列(一次性)
perm := rand.Perm(6)
for i := range perm {
fmt.Println(perm[i] + 1) // 输出 1–6 的某个随机顺序,无重复
}
}⚠️ 注意:rand.Seed() 不应在每次调用时重复执行(如原代码中放在 myrand := random(1,6) 前)。高频调用 time.Now().UnixNano() 可能因时间精度不足导致种子重复,反而降低随机性。推荐在 main() 开头或初始化阶段调用一次。
? 如何“记住上一次选到了哪”?—— 状态持久化是关键
你提到的核心困惑:“程序每次运行都新建,怎么知道上次用了哪个数?”——这正是该需求的技术难点。rand.Perm(6) 本身只生成一次序列,不会自动记忆当前索引。解决方案取决于你的运行环境:
-
单次长期运行服务(推荐):将排列切片和当前索引封装为结构体,通过变量/字段维护状态:
type RecipePicker struct { perm []int idx int } func NewRecipePicker() *RecipePicker { return &RecipePicker{ perm: rand.Perm(6), idx: 0, } } func (p *RecipePicker) Next() int { if p.idx >= len(p.perm) { p.perm = rand.Perm(6) // 耗尽后重新洗牌 p.idx = 0 } val := p.perm[p.idx] + 1 p.idx++ return val } // 使用示例 picker := NewRecipePicker() fmt.Println(picker.Next()) // 1st fmt.Println(picker.Next()) // 2nd // ... 自动循环 -
每次独立运行的 CLI 工具:必须借助外部存储(如文件、数据库)保存 lastIndex 和 lastPerm(或其种子)。例如写入 JSON 文件:
{ "perm": [2,5,0,4,1,3], "idx": 3 }下次启动时读取并恢复状态。注意并发写入需加锁。
? 整合到你的菜谱逻辑中(完整示例)
func printRecipeOfTheDay(recipe string) {
fmt.Printf("今日菜谱:%s\n", recipe)
}
func main() {
rand.Seed(time.Now().UnixNano())
picker := NewRecipePicker()
// 每日调用一次
switch picker.Next() {
case 1:
printRecipeOfTheDay("番茄炒蛋")
case 2:
printRecipeOfTheDay("红烧肉")
case 3:
printRecipeOfTheDay("清蒸鱼")
case 4:
printRecipeOfTheDay("麻婆豆腐")
case 5:
printRecipeOfTheDay("宫保鸡丁")
case 6:
printRecipeOfTheDay("青椒肉丝")
}
}✅ 总结
- ❌ 错误思路:用 rand.Intn() 配合“去重检查”——逻辑复杂、效率低、无法保证覆盖。
- ✅ 正确范式:rand.Perm(n) 生成全排列 → 线性遍历 → 耗尽后重置。
- ? 状态管理是核心:内存中维护(服务常驻)或持久化存储(短生命周期程序)。
- ? 切勿重复 Seed();使用 math/rand(非 crypto/rand)已足够满足此类非密码学场景。
此方案兼顾简洁性、可预测性与公平性,是轮换类业务逻辑的工业级实践。

















