我只想要一个随机字符串(大写或小写),没有数字,在 Go 中。最快和最简单的方法是什么?
原文由 Anish Shah 发布,翻译遵循 CC BY-SA 4.0 许可协议
我只想要一个随机字符串(大写或小写),没有数字,在 Go 中。最快和最简单的方法是什么?
原文由 Anish Shah 发布,翻译遵循 CC BY-SA 4.0 许可协议
你可以只为它编写代码。如果您希望在以 UTF-8 编码时所有字母都是单字节,则此代码可以稍微简单一些。
package main
import (
"fmt"
"time"
"math/rand"
)
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
func randSeq(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}
func main() {
rand.Seed(time.Now().UnixNano())
fmt.Println(randSeq(10))
}
原文由 Paul Hankin 发布,翻译遵循 CC BY-SA 4.0 许可协议
7 回答5.3k 阅读
6 回答6.9k 阅读✓ 已解决
4 回答2.9k 阅读
4 回答2.3k 阅读
1 回答2.7k 阅读✓ 已解决
1 回答3.4k 阅读
2 回答879 阅读✓ 已解决
保罗的解决方案 提供了一个 _简单_、通用的解决方案。
该问题要求 “最快,最简单的方法” 。让我们也解决 最快 的部分。我们将以迭代的方式到达我们最终的、最快的代码。可以在答案末尾找到每次迭代的基准测试。
所有解决方案和基准测试代码都可以在 Go Playground 上找到。 Playground 上的代码是测试文件,而不是可执行文件。您必须将其保存到名为
XX_test.go
的文件中并运行它前言:
话虽如此,即使您不需要最快的解决方案,通读此答案也可能具有冒险精神和教育意义。
一、改进
1.创世纪(符文)
提醒一下,我们正在改进的原始通用解决方案是:
2. 字节
如果要从中选择并组装随机字符串的字符仅包含英文字母的大写和小写字母,我们只能使用字节,因为英文字母映射到 UTF-8 编码中的字节 1 对 1(这是 Go 存储字符串的方式)。
所以不是:
我们可以用:
或者更好:
现在这已经是一个很大的改进:我们可以将它实现为
const
(有string
常量,但 没有切片常量)。作为额外的收获,表达式len(letters)
也将是const
! (表达式len(s)
是常量,如果s
是一个字符串常量。)代价是什么?什么都没有。
string
s can be indexed which indexes its bytes,完美,正是我们想要的。我们的下一个目的地是这样的:
3.余数
以前的解决方案通过调用
rand.Intn()
获得一个随机数来指定一个随机字母,它委托给Rand.Intn()
委托给Rand.Int31n()
。与
rand.Int63()
相比,这要慢得多,它产生一个具有 63 个随机位的随机数。所以我们可以简单地调用
rand.Int63()
并使用除以len(letterBytes)
后的余数:这有效并且明显更快,缺点是所有字母的概率不会完全相同(假设
rand.Int63()
以相等的概率产生所有 63 位数字)。虽然失真非常小,因为字母的数量52
比1<<63 - 1
小得多,所以在实践中这是非常好的。为了使这一点更容易理解:假设您想要
0..5
范围内的随机数。使用 3 个随机位,这将产生数字0..1
的概率是范围2..5
的两倍。 Using 5 random bits, numbers in range0..1
would occur with6/32
probability and numbers in range2..5
with5/32
probability which is now closer到想要的。增加位数会使这一点变得不那么重要,当达到 63 位时,它可以忽略不计。4.遮蔽
在之前的解决方案的基础上,我们可以通过仅使用尽可能多的随机数最低位来表示字母数来保持字母的平均分布。因此,例如,如果我们有 52 个字母,则需要 6 位来表示它:
52 = 110100b
。所以我们将只使用rand.Int63()
返回的数字的最低 6 位。为了保持字母的平均分配,我们只“接受”落在范围内的数字0..len(letterBytes)-1
。如果最低位更大,我们将其丢弃并查询一个新的随机数。请注意,最低位大于或等于
len(letterBytes)
的机会小于0.5
一般(0.25
),这意味着即使平均如果是这种情况,重复这种“罕见”情况会减少找不到合适数字的机会。在n
重复之后,我们仍然没有好的索引的机会远小于pow(0.5, n)
,这只是一个较高的估计。在 52 个字母的情况下,6 个最低位不好的机会只有(64-52)/64 = 0.19
;这意味着,例如,在 10 次重复后没有好数字的机会是1e-8
。所以这是解决方案:
5. 蒙版改进
先前的解决方案仅使用
rand.Int63()
返回的 63 个随机位中的最低 6 位。这是一种浪费,因为获取随机位是我们算法中最慢的部分。如果我们有 52 个字母,这意味着 6 位编码一个字母索引。所以 63 个随机位可以指定
63/6 = 10
不同的字母索引。让我们使用所有这 10 个:6.来源
Masking Improved 非常好,我们可以改进的不多。我们可以,但不值得这么复杂。
现在让我们找到其他需要改进的地方。 随机数的来源。
有一个
crypto/rand
包,它提供了一个Read(b []byte)
函数,所以我们可以使用它来通过一次调用获得我们需要的尽可能多的字节。这对性能没有帮助,因为crypto/rand
实现了加密安全的伪随机数生成器,因此速度要慢得多。所以让我们坚持使用
math/rand
包。rand.Rand
使用rand.Source
作为随机位的来源。rand.Source
是一个接口,它指定了一个Int63() int64
方法:正是我们在最新的解决方案中需要和使用的唯一方法。所以我们真的不需要
rand.Rand
(无论是显式的还是全局的,共享一个rand
包),一个rand.Source
:deb66f- 对我们来说已经足够了另请注意,最后一个解决方案不需要您初始化(种子)全局
Rand
math/rand
包因为未使用(和我们的rand.Source
已正确初始化/播种)。这里还有一件事要注意:
math/rand
的包文档指出:因此,默认源比
Source
获得的rand.NewSource()
,因为默认源必须在并发访问/使用下提供安全性,而rand.NewSource()
不提供这个(因此它返回的Source
更有可能更快)。7.利用
strings.Builder
所有以前的解决方案都返回一个
string
其内容首先构建在一个切片中([]rune
在 Genesis 和[]byte
中),然后转换为后续解决方案string
。最后的转换必须复制切片的内容,因为string
值是不可变的,如果转换不进行复制,则无法保证字符串的内容不会通过其原始内容进行修改片。详见 如何将utf8字符串转为[]byte? 和 golang: []byte(string) 与 []byte(*string) 。Go 1.10 引入了
strings.Builder
。strings.Builder
是一种新类型,我们可以用来构建string
的内容,类似于bytes.Buffer
。在内部它使用[]byte
构建内容,完成后,我们可以使用其Builder.String()
方法获得最终的string
值。但它的酷之处在于它无需执行我们刚才谈到的复制即可执行此操作。它之所以敢这样做,是因为用于构建字符串内容的字节片没有暴露,因此可以保证没有人可以无意或恶意地修改它来改变生成的“不可变”字符串。所以我们的下一个想法是不在切片中构建随机字符串,而是借助
strings.Builder
,所以一旦完成,我们就可以获得并返回结果,而无需复制它。这可能有助于提高速度,而且肯定有助于内存使用和分配。请注意,在创建一个新的
strings.Buidler
之后,我们调用了它的Builder.Grow()
方法,确保它分配了一个足够大的内部切片(以避免在我们添加随机字母时重新分配)。8.“模仿”
strings.Builder
包unsafe
strings.Builder
在内部构建字符串[]byte
,就像我们自己做的一样。所以基本上通过strings.Builder
做它有一些开销,我们唯一切换到strings.Builder
的目的是避免切片的最终复制。strings.Builder
通过使用包unsafe
--- 避免最终复制:问题是,我们也可以自己做。所以这里的想法是切换回在
[]byte
中构建随机字符串,但是当我们完成后,不要将它转换为string
返回,而是做一个不安全的转换:获取一个string
作为字符串数据指向我们的字节片。这是可以做到的:
(9. 使用
rand.Read()
)Go 1.7 添加 了一个
rand.Read()
函数和一个Rand.Read()
方法。为了获得更好的性能,我们应该尝试使用这些来一次读取尽可能多的字节。这有一个小“问题”:我们需要多少字节?我们可以说:与输出字母的数量一样多。我们会认为这是一个较高的估计,因为字母索引使用不到 8 位(1 字节)。但在这一点上,我们已经做得更糟了(因为获取随机位是“困难的部分”),而且我们得到的比需要的多。
另请注意,为了保持所有字母索引的均匀分布,可能会有一些我们无法使用的“垃圾”随机数据,因此我们最终会跳过一些数据,因此在我们遍历所有字母时最终会变短字节片。我们需要进一步“递归地”获得更多的随机字节。现在我们甚至失去了“单次调用
rand
包”的优势……我们可以“稍微”优化我们从
math.Rand()
获取的随机数据的使用。我们可以估计我们需要多少字节(位)。 1 个字母需要letterIdxBits
位,而我们需要n
字母,所以我们需要n * letterIdxBits / 8.0
字节四舍五入。我们可以计算随机索引不可用的概率(见上文),因此我们可以请求更多“更有可能”足够的索引(如果结果不可用,我们重复该过程)。例如,我们可以将字节片作为“比特流”处理,为此我们有一个很好的第三方库:github.com/icza/bitio
(披露:我是作者)。但是基准代码仍然显示我们没有获胜。为什么会这样?
最后一个问题的答案是因为
rand.Read()
使用了一个循环并不断调用Source.Int63()
直到它填充传递的切片。正是RandStringBytesMaskImprSrc()
解决方案的作用, 没有 中间缓冲区,也没有增加复杂性。这就是为什么RandStringBytesMaskImprSrc()
仍然在宝座上。是的,RandStringBytesMaskImprSrc()
使用不同步的rand.Source
与rand.Read()
--- 不同。但是推理仍然适用;如果我们使用Rand.Read()
而不是rand.Read()
就证明了这一点(前者也是不同步的)。二。基准
好吧,是时候对不同的解决方案进行基准测试了。
关键时刻:
仅通过从符文切换到字节,我们立即获得 24% 的性能提升,内存需求下降到 三分之一。
摆脱
rand.Intn()
并使用rand.Int63()
又提供了 20% 的提升。屏蔽(在大索引的情况下重复)会稍微慢一点(由于重复调用):- 22% …
但是,当我们使用 63 个随机位中的全部(或大部分)(来自一个
rand.Int63()
调用的 10 个索引)时:这大大加快了时间: 3 倍。如果我们使用(非默认的,新的)
rand.Source
而不是rand.Rand
,我们再次获得 21%。如果我们利用
strings.Builder
,我们的 速度 将获得微小的 3.5% ,但我们也实现了 50% 的内存使用和分配减少!那很好!最后,如果我们敢于使用包
unsafe
而不是strings.Builder
,我们将再次获得不错的 14% 。将最终解决方案与初始解决方案进行比较:
RandStringBytesMaskImprSrcUnsafe()
比RandStringRunes()
快6.3倍,使用 六分之一 的内存和 一半的分配。任务完成。