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

新手上路,请多包涵

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

原文由 Anish Shah 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 1.4k
2 个回答

保罗的解决方案 提供了一个 _简单_、通用的解决方案。

该问题要求 “最快,最简单的方法” 。让我们也解决 最快 的部分。我们将以迭代的方式到达我们最终的、最快的代码。可以在答案末尾找到每次迭代的基准测试。

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

go test -bench . -benchmem

前言

如果您只需要一个随机字符串,那么最快的解决方案不是首选解决方案。为此,保罗的解决方案是完美的。这是性能确实重要的情况。尽管前 2 个步骤( 字节 数和 剩余数)可能是一个可以接受的折衷方案:它们确实将性能提高了 50%(请参阅 II.Benchmark 部分中的确切数字),并且它们不会显着增加复杂性。

话虽如此,即使您不需要最快的解决方案,通读此答案也可能具有冒险精神和教育意义。

一、改进

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(这是 Go 存储字符串的方式)。

所以不是:

 var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

我们可以用:

 var letters = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

或者更好:

 const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

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

代价是什么?什么都没有。 string s can be indexed which indexes its bytes,完美,正是我们想要的。

我们的下一个目的地是这样的:

 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 个随机位的随机数。

所以我们可以简单地调用 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 位数字)。虽然失真非常小,因为字母的数量 521<<63 - 1 小得多,所以在实践中这是非常好的。

为了使这一点更容易理解:假设您想要 0..5 范围内的随机数。使用 3 个随机位,这将产生数字 0..1 的概率是范围 2..5 的两倍。 Using 5 random bits, numbers in range 0..1 would occur with 6/32 probability and numbers in range 2..5 with 5/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

所以这是解决方案:

 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 个字母,这意味着 6 位编码一个字母索引。所以 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.来源

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- 对我们来说已经足够了

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)
}

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

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

默认的 Source 对于多个 goroutines 并发使用是安全的。

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

7.利用 strings.Builder

所有以前的解决方案都返回一个 string 其内容首先构建在一个切片中( []runeGenesis[]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 ,所以一旦完成,我们就可以获得并返回结果,而无需复制它。这可能有助于提高速度,而且肯定有助于内存使用和分配。

 func RandStringBytesMaskImprSrcSB(n int) string {
    sb := strings.Builder{}
    sb.Grow(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) {
            sb.WriteByte(letterBytes[idx])
            i--
        }
        cache >>= letterIdxBits
        remain--
    }

    return sb.String()
}

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

8.“模仿” strings.Builderunsafe

strings.Builder 在内部构建字符串 []byte ,就像我们自己做的一样。所以基本上通过 strings.Builder 做它有一些开销,我们唯一切换到 strings.Builder 的目的是避免切片的最终复制。

strings.Builder 通过使用包 unsafe --- 避免最终复制:

 // String returns the accumulated string.
func (b *Builder) String() string {
    return *(*string)(unsafe.Pointer(&b.buf))
}

问题是,我们也可以自己做。所以这里的想法是切换回在 []byte 中构建随机字符串,但是当我们完成后,不要将它转换为 string 返回,而是做一个不安全的转换:获取一个 string 作为字符串数据指向我们的字节片。

这是可以做到的:

 func RandStringBytesMaskImprSrcUnsafe(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)(unsafe.Pointer(&b))
}

(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.Sourcerand.Read() --- 不同。但是推理仍然适用;如果我们使用 Rand.Read() 而不是 rand.Read() 就证明了这一点(前者也是不同步的)。

二。基准

好吧,是时候对不同的解决方案进行基准测试了。

关键时刻:

 BenchmarkRunes-4                     2000000    723 ns/op   96 B/op   2 allocs/op
BenchmarkBytes-4                     3000000    550 ns/op   32 B/op   2 allocs/op
BenchmarkBytesRmndr-4                3000000    438 ns/op   32 B/op   2 allocs/op
BenchmarkBytesMask-4                 3000000    534 ns/op   32 B/op   2 allocs/op
BenchmarkBytesMaskImpr-4            10000000    176 ns/op   32 B/op   2 allocs/op
BenchmarkBytesMaskImprSrc-4         10000000    139 ns/op   32 B/op   2 allocs/op
BenchmarkBytesMaskImprSrcSB-4       10000000    134 ns/op   16 B/op   1 allocs/op
BenchmarkBytesMaskImprSrcUnsafe-4   10000000    115 ns/op   16 B/op   1 allocs/op

仅通过从符文切换到字节,我们立即获得 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倍,使用 六分之一 的内存和 一半的分配。任务完成。

原文由 icza 发布,翻译遵循 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 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题