在Go语言中,我只需要一个随机字符串(大写或小写),没有数字。 最快和最简单的方法是什么?


Paul的解决方案提供了一个简单的通用解决方案。

好。

这个问题要求"最快和最简单的方法"。让我们也讨论最快的部分。我们将以迭代的方式得出最终的最快的代码。对每个迭代进行基准测试可以在答案的结尾处找到。

好。

所有解决方案和基准代码都可以在Go Playground上找到。 Playground上的代码是测试文件,而不是可执行文件。您必须将其保存到名为XX_test.go的文件中,然后使用

好。

前言:

好。

The fastest solution is not a go-to solution if you just need a random string. For that, Paul's solution is perfect. This is if performance does matter. Although the first 2 steps (Bytes and Remainder) might be an acceptable compromise: they do improve performance by like 50% (see exact numbers in the II. Benchmark section), and they don't increase complexity significantly.

Ok.

话虽如此,即使您不需要最快的解决方案,通读此答案也可能是冒险和有益的。

好。

一,改进

1.创世记(符文)

提醒一下,我们正在改进的原始通用解决方案是:

好。

2.字节

如果要从中选择和组合随机字符串的字符仅包含英文字母的大写和小写字母,我们只能使用字节,因为英文字母映射为UTF-8编码中的1-to-1字节(是Go存储字符串的方式)。

好。

所以代替:

好。

我们可以用:

好。

甚至更好:

好。

现在,这已经是一个很大的改进:我们可以将其实现为const(存在string常量,但是没有切片常量)。作为额外的收获,表达式len(letters)也将是const! (如果s是字符串常量,则表达式len(s)是常量。)

好。

而且要花多少钱?没事可以对string进行索引,从而为我们的字节建立索引,这正是我们想要的。

好。

我们的下一个目的地如下所示:

好。

3.剩余

先前的解决方案通过调用将rand.Intn()委托给rand.Intn()并将其委托给Rand.Int31n()的方法来获得随机数以指定随机字母。

好。

rand.Int63()相比,它要慢得多,该rand.Int63()产生具有63个随机位的随机数。

好。

因此,我们可以简单地调用rand.Int63()并除以len(letterBytes)后使用余数:

好。

这行之有效,而且速度更快,缺点是所有字母的概率不会完全相同(假设rand.Int63()产生所有63位数字的概率均相等)。尽管由于字母52的数量比1<<63 - 1小得多,所以失真非常小,所以在实践中,这非常好。

好。

为了使理解更容易:假设您想要一个在0..5范围内的随机数。使用3个随机位,这将产生比范围2..5更大的几率0..1。使用5个随机位,范围0..1中的数字将以6/32概率出现,范围2..5中的数字将以5/32概率出现,现在更接近所需值。增加位数会使此重要性降低,当达到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.来源

改进的遮罩效果非常好,我们可以对其进行改进。我们可以,但不值得如此复杂。

好。

现在让我们找到其他需要改进的地方。随机数的来源。

好。

有一个crypto/rand包提供了一个Read(b []byte)函数,因此我们可以使用该包通过一次调用获得所需的字节数。这对性能没有帮助,因为crypto/rand实现了加密安全的伪随机数生成器,因此它要慢得多。

好。

因此,让我们坚持使用math/rand软件包。 rand.Rand使用rand.Source作为随机位的来源。 rand.Source是一个指定Int63() int64方法的接口:正是我们在最新解决方案中需要和使用的唯一方法。

好。

因此,我们实际上并不需要rand.Rand(显式或全局的,共享的rand包之一),对于我们来说rand.Source就足够了:

好。

还要注意,最后一个解决方案不需要您初始化(种子)math/rand程序包的全局rand,因为它没有被使用(并且我们的rand.Source已正确初始化/设定种子)。

好。

这里还要注意一件事:math/rand的程序包文档指出:

好。

The default Source is safe for concurrent use by multiple goroutines.

Ok.

因此,默认源比rand.NewSource()可能获得的Source慢,因为默认源必须在并发访问/使用下提供安全性,而rand.NewSource()不提供安全性(因此返回Source通过它更有可能更快)。

好。

7.使用strings.Builder

以前的所有解决方案均返回string,其内容首先构建在切片中(Genesis中的[]rune,后续解决方案中的[]byte),然后转换为string。最后的转换必须复制切片的内容,因为string值是不可变的,并且如果转换不能复制,则不能保证字符串的内容不会通过其原始切片进行修改。有关详细信息,请参见如何将utf8字符串转换为[] byte?和golang:[] byte(string)与[] byte(* string)。

好。

Go 1.10引入了strings.Builderstrings.Builder一种新类型,可用于构建与bytes.Buffer类似的string内容。它在内部使用[]byte进行操作,完成后,我们可以使用其Builder.String()方法获得最终的string值。但是,最酷的是它可以执行此操作而不执行我们上面刚刚谈到的复制操作。之所以敢于这样做,是因为未暴露用于构建字符串内容的字节片,因此可以确保没有人可以无意或恶意地修改它来更改生成的"不可变"字符串。

好。

因此,我们的下一个想法是不要在切片中构建随机字符串,而是借助strings.Builder进行构建,因此一旦完成,我们就可以获取并返回结果,而无需对其进行复制。这可能会在速度方面有所帮助,并且绝对会在内存使用和分配方面有所帮助。

好。

请注意,在创建新的strings.Buidler之后,我们调用了它的Builder.Grow()方法,确保它分配了足够大的内部切片(以避免在添加随机字母时发生重新分配)。

好。

8.使用包unsafe"模仿" strings.Builder

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()保留在宝座上的原因。是的,与rand.Read()不同,RandStringBytesMaskImprSrc()使用的是未同步的rand.Source。但是推理仍然适用。如果我们使用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倍,使用六分之一的内存和一半的分配空间。 任务完成。

好。

好。


您可以为此编写代码。如果要以UTF-8编码时都依赖于全部为单个字节的字母,则此代码可以更简单一些。


两种可能的选择(当然可能还有更多选择):

  • 您可以使用crypto/rand软件包,该软件包支持读取随机字节数组(从/ dev / urandom),并且适用于加密随机生成。参见http://golang.org/pkg/crypto/rand/#example_Read。不过,它可能比正常的伪随机数生成慢。

  • 取一个随机数,然后使用md5或类似的方式对其进行哈希处理。


  • 使用软件包uniuri,该软件包将生成加密安全的统一(无偏)字符串。

    免责声明:我是包裹的作者


    icza's精彩解释的解决方案之后,这是使用crypto/rand而不是math/rand的修改。

    如果您想要一个更通用的解决方案,该解决方案允许您传入字符字节片以创建字符串,则可以尝试使用以下方法:

    如果您想传递自己的随机性来源,则修改上面的内容以接受io.Reader而不是使用crypto/rand将是微不足道的。


    如果要使用密码安全的随机数,并且确切的字符集是灵活的(例如,base64很好),则可以从所需的输出大小中准确计算出所需的随机字符长度。

    基数64的文本比基数256长1/3。(2 ^ 8 vs 2 ^ 6; 8bits / 6bits = 1.333 ratio)

    注意:如果您喜欢+和/而不是-和_,也可以使用RawStdEncoding

    如果要使用十六进制,则基数16比基数256长2倍。(2 ^ 8与2 ^ 4; 8bits / 4bits = 2x ratio)

    但是,如果您的字符集具有从base256到baseN的编码器,则可以将其扩展到任意字符集。您可以使用相同的大小进行计算,以表示字符集需要多少位。任何任意字符集的比率计算为:ratio = 8 / log2(len(charset)))。

    尽管这两种解决方案都是安全,简单,应该快速的,并且不会浪费您的加密熵池。

    这是操场显示它适用于任何大小。 https://play.golang.org/p/i61WUVR8_3Z



    这是我的方式)随意使用数学兰特或加密兰特。


    如果您愿意在允许的字符池中添加一些字符,则可以使该代码与通过io.Reader提供随机字节的任何内容一起使用。在这里,我们使用crypto/rand


    BenchmarkRandStr16-8 20000000 68.1 ns / op 16 B / op 1 allocs / op


    此外,我还找到了一个包,其中包含一堆用于处理假数据的方法。发现它在开发https://github.com/Pallinder/go-randomdata时对播种数据库很有用。可能对其他人也有帮助