回答

收藏

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

技术问答 技术问答 248 人阅读 | 0 人回复 | 2023-09-12

我想要一个没有数字的随机字符串(大写或小写)Go中。实施此操作最快、最简单的方法是什么?
! l, @4 t, U' P8 `3 q$ e                                                                ' z/ }3 [' M& X6 y0 q( f
    解决方案:                                                                - S! _6 E- }5 X; D9 i# G" k% [
                                                                问题要求最快最简单的方法。让我们解决最快的部分。我们将通过迭代获得最终和最快的代码。每次迭代的基准测试都可以在答案的最后找到。$ I# S' f0 }; M# \
所有的解决方案和基准测试代码都可以Go Playground上找到。Playground 上面的代码是一个测试文件,而不是一个可执行的文件。您必须将其保存在名称文件中XX_test.go并运行它
; S" o& p( I; p" ?
    go test -bench . -benchmem
    $ P" P' `& r$ G/ m) f
前言4 c+ n6 J8 p* U/ x: C
如果你只需要一个随机字符串,最快的解决方案不是首选。因此,保罗的解决方案是完美的。这就是性能是否重要。尽管前两步(BytesRemainder)这可能是一个可以接受的折衷方案:它们确实将性能提高了50%(见II. Benchmark部分中的确切数字),不会显著增加复杂性。
. F5 s* X  w4 m) a尽管如此,即使你不需要最快的解决方案,通读这个答案也可能具有冒险精神和教育意义。
$ W* C/ S5 w+ z: O( n% d( d一、改进1. Genesis (Runes)我们正在改进的原始通用解决方案是:, k  |- l0 |6 `) c$ r3 p, v
    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 = letterRunes[rand.Intn(len(letterRunes))]    }    return string(b)}2 D/ C8 x. f) \# h  I) Z$ {
2. 字节如果要选择和组合随机字符串的字符,只包括英文字母的大写和小写字母,我们只能使用字节,因为英文字母映射到 UTF-8 编码中的字节 1 至 1(包括 Go 存储字符串的方式)。
& q' u7 [0 ^4 J, e5 [所以不是:
4 y9 ^; Q  `0 w; a& U. Z0 D
    var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")+ b7 i5 Y4 X* I8 d; J. g
我们可以用:/ R( L# c: S: |, Q& R
    var letters = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")! j$ V! F# B4 b# p$ \2 R
或至更好:1 n. R5 B+ Q" w, x
    const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"& S" K0 C& Y% r% `
现在这已经是一个很大的改进:我们可以将它实现为 a const(有string常量但无切片常量)。作为额外的收获,表达式len(letters)也将是const! (len(s)如果s字符串常量,表达式为常量。4 ^$ l3 G& X7 j8 f2 j
什么代价?什么都没有。strings 可以索引,索引它的字节,完美,正是我们想要的。
* k: B; y$ a/ F/ H下一个目的地是这样的:
- `$ p$ v2 B5 g* b' a
    const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"func RandStringBytes(n int) string    b := make([]byte,n)    for i := range b        b = letterBytes[rand.Intn(len(letterBytes))]   }    return string(b)}* X( ^: I& \9 f  d
3. 余数以前的解决方案通过调用rand.Intn()委托给什么?Rand.Intn()什么样的委托来指定随机字母?Rand.Int31n()。% i: Q& u1 A5 {6 w, b
与rand.Int与 63 个随机位的随机数相比,生成的随机数要慢得多。
" b! d& ?* _/ s5 U6 b因此,我们可以简单地调用它rand.Int63()并使用除以后的余数len(letterBytes):
! w6 S7 U0 _0 F( o0 O7 s7 G
    func RandStringBytesRmndr(n int) string    b := make([]byte,n)    for i := range b        b = letterBytes[rand.Int63() % int64(len(letterBytes))]   }    return string(b)}
    - X( O# r7 d% V& \  n5 a1 r$ V
缺点是所有字母的概率点是所有字母的概率不会完全相同(假设rand.Int63()产生所有 63 位数的概率相等)。虽然字母的数量远小于1
* O3 K1 Z1 T- o3 J3 @9 z" E假设你想要一个范围为 的随机数0,以使它更容易理解:..5。使用 3 个随机位,这将产生0..1比范围 2 倍概率数字2..5.数字范围为0,使用5个随机比特..6/32概率和数字范围的2..5和5/32的概率现在更接近预期。当达到 63 位时,增加位数会使这一点变得不那么重要。
/ T' a8 X  U3 x1 y+ {8 A7 p4.  Masking在以前的解决方案的基础上,我们可以使用与字母数量相同的最低数量来保持字母的均匀分布。因此,例如,如果我们有52个,它需要6个来表示它:52 = 110100b。因此,我们只使用 返回数字的最低 6 位rand.Int63()。为了保持字母的平均分布,我们只接受范围内的数字0..len(letterBytes)-1.如果最低水平较大,我们将丢弃它并查询新的随机数。* H) o2 o/ G4 z- _; I" ^) a2 ?
请注意,最低机会大于或等于len(letterBytes)通常小于0.5(0.25平均),这意味着即使在这种情况下,重复这种罕见的情况也会减少找不到好机会的数字。n重复后,我们仍然没有好的索引机会远远小于pow(0.5,n),这只是一个上面的估计。在 52 字母的情况下,只有 6 最低不好的机会(64-52)/64 = 0.19;这意味着例如, 10 重复后没有好数字的机会是1e-8./ d% @4 w6 K% p7 ^
所以这里是解决方案:8 P4 d0 b' d. W' _+ v7 L
    & |6 H4 Z+ c7 w; Q4 U6 L
  • const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"const  letterIdxBits =                   bits to represent a letter index    letterIdxMask = 15. Masking改进前面的解决方案只使用 返回的 63 个随机位中最低 6 位rand.Int63()。这是一种浪费,因为获取随机位是我们算法中最慢的部分。8 I# ?+ p$ l5 w
  • 如果我们有 52 字母,这意味着 6 编码一个字母索引。因此, 63 随机位可指定63/6 = 10不同的字母索引。让我们使用所有这些 10 :[code]const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"const  letterIdxBits =                   bits to represent a letter index    letterIdxMask = 1= if remain ==             cache,remain = rand.Int63(),letterIdxMax          if idx := int(cache & letterIdxMask); idx >= letterIdxBits        remain--   }    return string(b)}
    ' ^+ }3 l3 @3 G) n7 Q* [9 f7 _
6. 来源该改进的屏蔽挺好的,改进不了多少。我们可以,但不值得复杂。& U7 ?: a) [1 A$ r4 }
现在让我们找到其他需要改进的地方。随机来源。
; H5 B; Z% T$ e& @5 m" S有一个crypto/rand提供一个包Read(b [\]byte)函数,所以我们可以用它来获得尽可能多的字节。这对性能没有帮助,因为crypto/rand实现加密安全伪随机数生成器,因此速度要慢得多。1 I1 d6 a0 `+ j! H2 |4 w8 i0 a! T0 P6 j
所以让我们坚持下去math/rand包装。在rand.Rand使用rand.Source作为随机比特的来源。rand.Source它指定了一个接口Int63() int64方法:这是我们在最新解决方案中需要和使用的唯一东西。) Q, ]1 ^0 @+ c& `: ^# Q
所以我们真的不需要一个rand.Rand(显式或全局,共享rand包),arand.Source对我们来说已经足够了:
1 L: _3 t2 ]* E1 E/ n( d  S
    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 >= letterIdxBits        remain--   }    return string(b)}& @6 p& A/ I7 P. M- r' Y8 E. d# d3 L
还需要注意的是,这个最终的解决方案并不要求你在全球初始化(种子)Rand的的math/rand包里没用(和我们一起)rand.Source正确的初始化/种子)。
& `) k- b' y& z. h. v: V1 ?这里还要注意一件事:math/rand状态包文件:6 k/ S- I! N- \( v
默认的 Source 可安全地被多个  goroutines 并发使用。
- L- @( B# c7 f, d. O因此,默认源比Source可能获得的a 慢rand.NewSource()因为默认源必须在并发访问/使用下提供安全,rand.NewSource()而不提供(因此Source返回更有可能更快)。
4 l! N" n: L4 @1 Q3 Y1 m7.利用 strings.Builder所有以前的解决方案都返回 a ,string其内容首先构建在切片中([]rune在Genesis和[]byte在后续的解决方案中)string. 最终的转换必须复制切片的内容,因为string值是不可变的。如果转换不能复制,则不能保证字符串的内容不会通过其原始切片进行修改。详情请参阅[如何使用 utf8 字符串转换为 ]byte?和[golang: ]byte(string) vs []byte(*string)。, F" W: c5 W5 P' r% W& @  H
Go 1.10 引入strings.Builder。strings.Builder我们可以用它来构建一种新型号string类似于bytes.Buffer. 在内部使用 a[]byte构建内容,当我们完成时,我们可以string使用它的Builder.String()获得最终值的方法。但很酷的是,它可以完成这个操作,而无需执行我们刚才提到的复制。它之所以敢这样做,是因为用来构建字符串内容的字节片没有暴露,所以确保没有人能无意或恶意地修改它来改变产生的不可变字符串。) w: E: ^/ [4 i! h% a  E/ z
所以我们的下一个想法不是在切片中构建随机字符串,而是在 a 的帮助下strings.Builder,因此,一旦我们完成,我们就可以在不复制的情况下获得并返回结果。这可能有助于速度,肯定有助于内存的使用和分配。) I/ @& J' ?) e! k* r
    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 >= letterIdxBits        remain--   }    return sb.String()}
    " r- d, h# m' \+ s& j0 `
请注意,注意new 之后strings.Buidler,我们调用了它的Builder.Grow()确保它分配足够大的内部切片(以避免在添加随机字母时重新分配)。9 o; o4 ]3 H" n7 h. I3 N1 G) ]4 m: ?
8.strings.Builder用包模仿unsafestrings.Builder在内部构建字符串,[]byte就像我们自己做的。所以基本上是通过 a 来做strings.Builder有一些开销,我们切换到的唯一一件事strings.Builder避免最终复制切片。2 l1 ]5 Z% I+ w; l$ H! J4 ~
strings.Builder通过使用 package 避免最终副本unsafe:
  S9 P# f/ @+ s, }4 ?0 }
    // String returns the accumulated string.func (b *Builder) String() string    return *(*string)(unsafe.Pointer(&b.buf))}8 U: k. f5 g1 ^  i
问题是我们也可以自己做。所以这里的想法是切换回 a 构建随机字符串[]byte,但是,当我们完成它时,不要转换它string为 return,相反,进行不安全转换:获取string 指向我们的字节切片作为字符串数据a .
1 f; K& r5 R) s5 s这是怎么做到的:7 ~+ b3 y1 p* p2 g; k
    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 >= letterIdxBits        remain--   }    return *(*string)(unsafe.Pointer(&b))}
    ; j" j8 @4 A" f" ]# Q# R
(9. 使用rand.Read())Go 1.7 加了一个rand.Read()函数和一个Rand.Read()方法。为了实现更好的性能,我们应该尝试使用这些来阅读我们需要尽可能多的字节。
$ j+ n% m# ~  k9 V  ^6 P有一个小问题:我们需要多少字节?我们可以说,字母的输出量与输出字母的数量相同。我们会认为这是一个上限估计,因为字母索引的使用量小于 8 (1 字节)。但在这一点上,我们做得更糟(因为获得随机位置是困难的部分),我们得到的超出了需求。. J& K8 Y7 S, M
此外,请注意,为了保持所有字母索引的均匀分布,可能会有一些我们不能使用的垃圾随机数据,所以我们最终会跳过一些数据,所以当我们通过所有数据时,我们最终会有一个短字节切片。我们需要进一步交付才能获得更多的随机字节。现在我们甚至失去了单呼叫rand包装的优点......
) I8 u5 d7 K2 `# t- g' v我们可以稍微优化我们获得的随机数据的使用math.Rand()。我们可以估计需要多少字节(位)。1 字母需要letterIdxBits我们需要位置n所以我们需要字母n * letterIdxBits / 8.0字节四舍五入。我们可以计算随机索引不可用的概率(见上述),因此我们可以要求更多的更有可能(如果没有,我们将重复此过程)。例如,我们可以将字节切片视为位流,因此我们有一个很好的 3rd 方库:(github.com/icza/bitio披露:我是作者)。5 Z: F) g8 y1 a! ^
但基准代码仍然表明我们没有获胜。为什么?4 \+ q4 K1 S. V/ O
最后一个问题的答案是因为rand.Read()使用循环并不断调用,Source.Int63()直到它填满传递的切片。RandStringBytesMaskImprSrc()解决方案所做的,没有中间缓冲区没有增加复杂性。这就是为什么RandStringBytesMaskImprSrc()留在宝座上。RandStringBytesMaskImprSrc()使用不同步的rand.Source不同rand.Read()Rand.Read()而不是证明这一点rand.Read()(前者也不同步)。
7 q. x3 F" r$ _2 H- g二、基准是时候基准测试不同的解决方案了。
4 r$ i; U7 S8 J5 O, o关键时刻:
: L& M, {! d* R7 M
    BenchmarkRunes-                 ns/op   96 B/op   2 allocs/opBenchmarkBytes-                        5500             ns/op   32 B/op   2 allocs/opBenchmarkBytesRmndr-                   3万0000000000000000000000000300000000000000000000000000000000000000000000000000000ns/op   32 B/op   2 allocs/opBenchmarkBytesMask-                   3万00000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ns/op   32 B/op   2 allocs/opBenchmarkBytesMaskImpr-           ns/op   32 B/op   2 allocs/opBenchmarkBytesMaskImprSrc-       ns/op   32 B/op   2 allocs/opBenchmarkBytesMaskImprSrcSB-         1000000000000000000000000ns/op   16 B/op   1 allocs/opBenchmarkBytesMaskImprSrcUnsafe- 115 ns/op   16 B/op   1 allocs/op- D) d+ X) T" g7 U) e/ t
我们立即从符文切换到字节24% 的性能提高,内存需求下降到三分之一
' h" q6 y* L) l8 ^1 u摆脱rand.Intn()并使用rand.Int63()它会带来另一个20% 的提升。
5 @: O/ w' q" T# @: N掩码(并在大索引的情况下重复)稍慢(因重复调用):- 22%    …0 @6 p: d$ B9 y6 ^
然而,当我们使用全部(或大部分)63个随机位(来自一次)时rand.Int63()调用10 索引时:这将大大加快时间:3 倍% O1 \" k: I" H2 p
假如我们使用(非默认,新的)rand.Source代替rand.Rand,我们再次获得21%。
1 b0 l( a6 B) ~* U) O$ j' Q, f& o1 m如果我们使用strings.Builder,我们得到了一个小的3.5%的速度,但是我们也得到了50%减少内存的使用和分配!那很好!
) G7 a' P7 O9 _: W+ m最后,如果我们敢用 packageunsafe而不是strings.Builder,我们又得到了好的东西14%
) D. M: A% O. }" W最后对初始解进行比较:RandStringBytesMaskImprSrcUnsafe()是快6.3倍比RandStringRunes(),使用六分之一存储器和尽量少分配半。任务完成。
分享到:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则