回答

收藏

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

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

我想要一个没有数字的随机字符串(大写或小写)Go中。实施此操作最快、最简单的方法是什么?( r' m- V3 R3 R+ y+ j: u5 r9 a' I) J
                                                               
+ K, |, u2 ^+ g    解决方案:                                                               
5 F7 }5 e' h, A4 t7 z6 ~. H" v                                                                问题要求最快最简单的方法。让我们解决最快的部分。我们将通过迭代获得最终和最快的代码。每次迭代的基准测试都可以在答案的最后找到。
/ e4 M, ~6 L, l4 \所有的解决方案和基准测试代码都可以Go Playground上找到。Playground 上面的代码是一个测试文件,而不是一个可执行的文件。您必须将其保存在名称文件中XX_test.go并运行它) x* L$ Y7 c& E/ N
    go test -bench . -benchmem, B, |% A4 L- u4 z0 T
前言
  R; B+ N4 D0 g; ?6 b, ~1 w+ a* W如果你只需要一个随机字符串,最快的解决方案不是首选。因此,保罗的解决方案是完美的。这就是性能是否重要。尽管前两步(BytesRemainder)这可能是一个可以接受的折衷方案:它们确实将性能提高了50%(见II. Benchmark部分中的确切数字),不会显著增加复杂性。0 Y- z# q# L9 `
尽管如此,即使你不需要最快的解决方案,通读这个答案也可能具有冒险精神和教育意义。. H# k2 v  C8 \' @5 o- b3 B+ \
一、改进1. Genesis (Runes)我们正在改进的原始通用解决方案是:
- l0 a9 h. `7 b7 B3 R/ I1 _6 f7 u1 m
    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)}# ^7 `; I7 g' g+ V0 j: T
2. 字节如果要选择和组合随机字符串的字符,只包括英文字母的大写和小写字母,我们只能使用字节,因为英文字母映射到 UTF-8 编码中的字节 1 至 1(包括 Go 存储字符串的方式)。; X8 X9 \$ k/ R9 ^) X8 C
所以不是:' e+ x( R6 O. h
    var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")8 [2 p0 x4 G: {. c" Q5 Q
我们可以用:, L- B; u$ ?4 y: \* R4 n
    var letters = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
    $ l1 O9 `# ]3 b+ ]
或至更好:
$ x$ s' M7 B( J0 h$ S* A7 P
    const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
    % N" R5 E4 b2 ^! ^" c( u; r
现在这已经是一个很大的改进:我们可以将它实现为 a const(有string常量但无切片常量)。作为额外的收获,表达式len(letters)也将是const! (len(s)如果s字符串常量,表达式为常量。* l  ^. v( T) c- s/ D) e
什么代价?什么都没有。strings 可以索引,索引它的字节,完美,正是我们想要的。
$ k# r0 Y% A& C9 e# e* |- r& y& p4 U' p下一个目的地是这样的:: A/ V# Z' T* x0 h5 s0 I6 m
    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)}
    % S" ~$ |0 ]( }- F2 H. k
3. 余数以前的解决方案通过调用rand.Intn()委托给什么?Rand.Intn()什么样的委托来指定随机字母?Rand.Int31n()。
2 ^( M5 m4 h3 V" a与rand.Int与 63 个随机位的随机数相比,生成的随机数要慢得多。
* J3 r' q( s1 L% ?% ]- E5 Y% O, p因此,我们可以简单地调用它rand.Int63()并使用除以后的余数len(letterBytes):
; c* A. z- {2 d& [, V; n
    func RandStringBytesRmndr(n int) string    b := make([]byte,n)    for i := range b        b = letterBytes[rand.Int63() % int64(len(letterBytes))]   }    return string(b)}
    . l3 n+ q9 S: D0 |. ~- G7 Q, T
缺点是所有字母的概率点是所有字母的概率不会完全相同(假设rand.Int63()产生所有 63 位数的概率相等)。虽然字母的数量远小于1! F6 s; O. `2 ^/ t' v! M5 Y
假设你想要一个范围为 的随机数0,以使它更容易理解:..5。使用 3 个随机位,这将产生0..1比范围 2 倍概率数字2..5.数字范围为0,使用5个随机比特..6/32概率和数字范围的2..5和5/32的概率现在更接近预期。当达到 63 位时,增加位数会使这一点变得不那么重要。
6 }3 M7 |& e: u9 b, T4.  Masking在以前的解决方案的基础上,我们可以使用与字母数量相同的最低数量来保持字母的均匀分布。因此,例如,如果我们有52个,它需要6个来表示它:52 = 110100b。因此,我们只使用 返回数字的最低 6 位rand.Int63()。为了保持字母的平均分布,我们只接受范围内的数字0..len(letterBytes)-1.如果最低水平较大,我们将丢弃它并查询新的随机数。, ^1 h5 V6 n$ B, [6 U8 M1 [5 w
请注意,最低机会大于或等于len(letterBytes)通常小于0.5(0.25平均),这意味着即使在这种情况下,重复这种罕见的情况也会减少找不到好机会的数字。n重复后,我们仍然没有好的索引机会远远小于pow(0.5,n),这只是一个上面的估计。在 52 字母的情况下,只有 6 最低不好的机会(64-52)/64 = 0.19;这意味着例如, 10 重复后没有好数字的机会是1e-8.
1 o9 W% v( N/ z所以这里是解决方案:! I. e& L( q* ]

    : E1 A/ I# T5 S6 O- G6 Z, X
  • const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"const  letterIdxBits =                   bits to represent a letter index    letterIdxMask = 15. Masking改进前面的解决方案只使用 返回的 63 个随机位中最低 6 位rand.Int63()。这是一种浪费,因为获取随机位是我们算法中最慢的部分。- B4 @( c) b& n; W3 @
  • 如果我们有 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)}, c& f$ b! X- Y1 {+ ^/ Z
6. 来源该改进的屏蔽挺好的,改进不了多少。我们可以,但不值得复杂。- f5 L; p( n! h) W
现在让我们找到其他需要改进的地方。随机来源。
) s( \; Z  ^1 r' H有一个crypto/rand提供一个包Read(b [\]byte)函数,所以我们可以用它来获得尽可能多的字节。这对性能没有帮助,因为crypto/rand实现加密安全伪随机数生成器,因此速度要慢得多。# t' L; X5 W$ h- c" ]
所以让我们坚持下去math/rand包装。在rand.Rand使用rand.Source作为随机比特的来源。rand.Source它指定了一个接口Int63() int64方法:这是我们在最新解决方案中需要和使用的唯一东西。* i" _6 l1 b1 X) d5 i+ v) h
所以我们真的不需要一个rand.Rand(显式或全局,共享rand包),arand.Source对我们来说已经足够了:- {1 F  }- F) U
    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)}. n* e5 d$ L! q
还需要注意的是,这个最终的解决方案并不要求你在全球初始化(种子)Rand的的math/rand包里没用(和我们一起)rand.Source正确的初始化/种子)。9 ^' S! G- x: p& h
这里还要注意一件事:math/rand状态包文件:" S1 k- O3 U% g
默认的 Source 可安全地被多个  goroutines 并发使用。  D3 u5 |; x7 I( X6 _
因此,默认源比Source可能获得的a 慢rand.NewSource()因为默认源必须在并发访问/使用下提供安全,rand.NewSource()而不提供(因此Source返回更有可能更快)。' u& y: \! c2 {) r0 l  [9 c
7.利用 strings.Builder所有以前的解决方案都返回 a ,string其内容首先构建在切片中([]rune在Genesis和[]byte在后续的解决方案中)string. 最终的转换必须复制切片的内容,因为string值是不可变的。如果转换不能复制,则不能保证字符串的内容不会通过其原始切片进行修改。详情请参阅[如何使用 utf8 字符串转换为 ]byte?和[golang: ]byte(string) vs []byte(*string)。% Z4 W, q5 b: v1 k3 ?
Go 1.10 引入strings.Builder。strings.Builder我们可以用它来构建一种新型号string类似于bytes.Buffer. 在内部使用 a[]byte构建内容,当我们完成时,我们可以string使用它的Builder.String()获得最终值的方法。但很酷的是,它可以完成这个操作,而无需执行我们刚才提到的复制。它之所以敢这样做,是因为用来构建字符串内容的字节片没有暴露,所以确保没有人能无意或恶意地修改它来改变产生的不可变字符串。8 U! O  r; y; Y' T
所以我们的下一个想法不是在切片中构建随机字符串,而是在 a 的帮助下strings.Builder,因此,一旦我们完成,我们就可以在不复制的情况下获得并返回结果。这可能有助于速度,肯定有助于内存的使用和分配。
+ J: a9 j+ i( [: m6 F
    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()}
    ! P: b# ~4 ?3 Q6 f% v1 Y
请注意,注意new 之后strings.Buidler,我们调用了它的Builder.Grow()确保它分配足够大的内部切片(以避免在添加随机字母时重新分配)。  K, J9 E, U% F, S2 m3 ~: |; o
8.strings.Builder用包模仿unsafestrings.Builder在内部构建字符串,[]byte就像我们自己做的。所以基本上是通过 a 来做strings.Builder有一些开销,我们切换到的唯一一件事strings.Builder避免最终复制切片。
/ s4 \. ~, _. R$ g% e! hstrings.Builder通过使用 package 避免最终副本unsafe:0 z! S# ^. X" P2 z2 Y& T! N- t
    // String returns the accumulated string.func (b *Builder) String() string    return *(*string)(unsafe.Pointer(&b.buf))}( A! \. V! N' i, d
问题是我们也可以自己做。所以这里的想法是切换回 a 构建随机字符串[]byte,但是,当我们完成它时,不要转换它string为 return,相反,进行不安全转换:获取string 指向我们的字节切片作为字符串数据a .* @$ |: y3 I4 e! F; a3 s4 R  v( U
这是怎么做到的:5 H5 U" l6 r- L7 D
    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))}
      Q0 ?; q& P/ _1 T$ d9 |1 s
(9. 使用rand.Read())Go 1.7 加了一个rand.Read()函数和一个Rand.Read()方法。为了实现更好的性能,我们应该尝试使用这些来阅读我们需要尽可能多的字节。
8 F8 K4 a9 E2 f有一个小问题:我们需要多少字节?我们可以说,字母的输出量与输出字母的数量相同。我们会认为这是一个上限估计,因为字母索引的使用量小于 8 (1 字节)。但在这一点上,我们做得更糟(因为获得随机位置是困难的部分),我们得到的超出了需求。
. J3 B8 d1 G1 o4 j此外,请注意,为了保持所有字母索引的均匀分布,可能会有一些我们不能使用的垃圾随机数据,所以我们最终会跳过一些数据,所以当我们通过所有数据时,我们最终会有一个短字节切片。我们需要进一步交付才能获得更多的随机字节。现在我们甚至失去了单呼叫rand包装的优点......: t8 Y2 P' m3 x6 Q7 z: O
我们可以稍微优化我们获得的随机数据的使用math.Rand()。我们可以估计需要多少字节(位)。1 字母需要letterIdxBits我们需要位置n所以我们需要字母n * letterIdxBits / 8.0字节四舍五入。我们可以计算随机索引不可用的概率(见上述),因此我们可以要求更多的更有可能(如果没有,我们将重复此过程)。例如,我们可以将字节切片视为位流,因此我们有一个很好的 3rd 方库:(github.com/icza/bitio披露:我是作者)。
* C7 f2 q# ~6 `' ]但基准代码仍然表明我们没有获胜。为什么?
9 n$ ^$ t! Q: l, X( S- k最后一个问题的答案是因为rand.Read()使用循环并不断调用,Source.Int63()直到它填满传递的切片。RandStringBytesMaskImprSrc()解决方案所做的,没有中间缓冲区没有增加复杂性。这就是为什么RandStringBytesMaskImprSrc()留在宝座上。RandStringBytesMaskImprSrc()使用不同步的rand.Source不同rand.Read()Rand.Read()而不是证明这一点rand.Read()(前者也不同步)。
8 D8 f$ f2 K& A9 {! C: W8 Y二、基准是时候基准测试不同的解决方案了。( G5 g! g# \& f& Y
关键时刻:
6 S; H4 T. [7 Z2 e$ [
    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
      ^/ a1 J' O! n9 W$ q7 }  a
我们立即从符文切换到字节24% 的性能提高,内存需求下降到三分之一  g* P/ L9 t) o9 U1 |
摆脱rand.Intn()并使用rand.Int63()它会带来另一个20% 的提升。4 O9 ?, }; W: {* X
掩码(并在大索引的情况下重复)稍慢(因重复调用):- 22%    …
( @0 s! ?+ X8 M) J, ]然而,当我们使用全部(或大部分)63个随机位(来自一次)时rand.Int63()调用10 索引时:这将大大加快时间:3 倍
' P* k" W) l5 [$ M: d# N假如我们使用(非默认,新的)rand.Source代替rand.Rand,我们再次获得21%。
. G% E1 w6 G/ v9 v  Z# T. R' M; D如果我们使用strings.Builder,我们得到了一个小的3.5%的速度,但是我们也得到了50%减少内存的使用和分配!那很好!
- d8 O% _9 r  M7 n; K5 B# l8 O最后,如果我们敢用 packageunsafe而不是strings.Builder,我们又得到了好的东西14%
& K7 f3 _4 _! q+ l1 _最后对初始解进行比较:RandStringBytesMaskImprSrcUnsafe()是快6.3倍比RandStringRunes(),使用六分之一存储器和尽量少分配半。任务完成。
分享到:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则