当前位置: 首页>>技术问答>>正文


如何在Go中生成固定长度的随机字符串?

qingchuanTR 技术问答 , , , 去评论

如果想在Go语言中生成只有随机字符(大写或小写)、没有数字的定长字符串。什么方法最快最简单?

这个问题要求“最快最简单的方法”。我们将由浅入深,以循序渐进的方式给出最终最快的代码。(可以在答案的最后找到每次迭代的基准测试。)

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

Go语言随机字符串

一,逐步改进的方法

1.起步(字符)

我们需要改进的原始通用解决方案是:

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

var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

func RandStringRunes(n int) string {
    b := make([]rune, n)
    for i := range b {
        b[i] = letterRunes[rand.Intn(len(letterRunes))]
    }
    return string(b)
}

2.字节

如果要选择和汇总的字符来自随机字符串(只包含英文字母的大写和小写字母),我们可以使用字节,因为英文字母映射到UTF-8编码中的字节是1对1(是如何存储字符串)。

所以代替:

var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

我们可以用:

var letters = []bytes("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

甚至更好的:

const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

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

这样的性能开销是多少?几乎没有开销! string可以编入索引,索引其字节。完美,正是我们想要的。

所以我们的下一个方法如下:

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

func RandStringBytes(n int) string {
    b := make([]byte, n)
    for i := range b {
        b[i] = letterBytes[rand.Intn(len(letterBytes))]
    }
    return string(b)
}

3.求余数

之前的解决方案通过调用Rand.Intn()获得一个随机数来指定随机字母,rand.Intn()委托给Rand.Int31n()

rand.Int63()相比,这要慢得多,后者产生一个63随机bits的随机数。

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

func RandStringBytesRmndr(n int) string {
    b := make([]byte, n)
    for i := range b {
        b[i] = letterBytes[rand.Int63() % int64(len(letterBytes))]
    }
    return string(b)
}

这种方法效果明显更快,缺点是所有字母的概率都不完全相同(假设rand.Int63()以相同的概率产生所有63位数字)。尽管由于字母52的数量比1<<63 - 1小很多,但是失真非常小,所以在实践中这非常精细。

为了使这个理解更容易:假设你想要一个0..5范围内的随机数。使用3个随机位,这将产生具有双倍概率的数字0..1,而不是来自范围2..5。使用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),这意味着即使出现这种情况,重复此"rare"案例也会降低找不到的好的数字的可能性。在n次重复之后,我们没有获得好数字的机会远小于pow(0.5, n),这只是一个较高的估计。在52个字母的情况下,6个最低位不好的可能性仅为(64-52)/64 = 0.19;这意味着例如在10次重复之后没有良好数字的机会是1e-8

所以新的解决方案如下:

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
    letterIdxBits = 6                    // 6 bits to represent a letter index
    letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
)

func RandStringBytesMask(n int) string {
    b := make([]byte, n)
    for i := 0; i < n; {
        if idx := int(rand.Int63() & letterIdxMask); idx < len(letterBytes) {
            b[i] = letterBytes[idx]
            i++
        }
    }
    return string(b)
}

5.掩码改进

前面的解决方案仅使用rand.Int63()返回的63个随机位中的最低6位。这是一种浪费,因为获取随机位是我们算法中最慢的部分。

如果我们有52个字母,那意味着6bits编码字母索引。所以63个随机位可以指定63/6 = 10不同的字母索引。让我们使用所有这10个:

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
    letterIdxBits = 6                    // 6 bits to represent a letter index
    letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
    letterIdxMax  = 63 / letterIdxBits   // # of letter indices fitting in 63 bits
)

func RandStringBytesMaskImpr(n int) string {
    b := make([]byte, n)
    // A rand.Int63() generates 63 random bits, enough for letterIdxMax letters!
    for i, cache, remain := n-1, rand.Int63(), letterIdxMax; i >= 0; {
        if remain == 0 {
            cache, remain = rand.Int63(), letterIdxMax
        }
        if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
            b[i] = letterBytes[idx]
            i--
        }
        cache >>= letterIdxBits
        remain--
    }

    return string(b)
}

6.来源

掩码改进非常好,我们还可以改进它,但过于复杂以至于不值得这么做了。

现在让我们找到其他改进的点——看看随机数的来源。

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

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

所以我们真的不需要rand.Rand(显式或全局,共享的rand包),rand.Source对我们来说已经足够了:

var src = rand.NewSource(time.Now().UnixNano())

func RandStringBytesMaskImprSrc(n int) string {
    b := make([]byte, n)
    // A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
    for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
        if remain == 0 {
            cache, remain = src.Int63(), letterIdxMax
        }
        if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
            b[i] = letterBytes[idx]
            i--
        }
        cache >>= letterIdxBits
        remain--
    }

    return string(b)
}

另请注意,最后一个解决方案不要求您初始化(播种)math/rand软件包的全局Rand,因为它未被使用(并且我们的rand.Source已正确初始化/设置种子)。

还有一点需要注意:math/rand的包文档说明:

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

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

二、评测

好吧,让我们对不同的解决方案进行基准测试。

BenchmarkRunes                   1000000              1703 ns/op
BenchmarkBytes                   1000000              1328 ns/op
BenchmarkBytesRmndr              1000000              1012 ns/op
BenchmarkBytesMask               1000000              1214 ns/op
BenchmarkBytesMaskImpr           5000000               395 ns/op
BenchmarkBytesMaskImprSrc        5000000               303 ns/op

只需从字符串切换到字节,我们立即获得22%的性能提升。

摆脱rand.Intn()并使用rand.Int63()代替另外24%的提升。

掩码(并且在大索引的情况下重复)减慢一点(由于重复调用):-20%......

但是当我们使用63个随机位中的所有(或大多数)(来自一个rand.Int63()调用的10个索引)时:它加速了3.4倍。

最后,如果我们解决了(non-default,新)rand.Source代替rand.Rand,我们再次获得23%。

比较最终解决方案:RandStringBytesMaskImprSrc()RandStringRunes()快5.6倍。

Go语言随机字符串2

参考资料

 

本文由《纯净的天空》出品。文章地址: https://vimsky.com/article/4115.html,未经允许,请勿转载。