栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 面试经验 > 面试问答

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

面试问答 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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

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

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

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

XX_test.go
,然后使用

go test -bench . -benchmem

前言

如果只需要随机字符串,最快的解决方案不是首选解决方案。为此,保罗的解决方案是完美的。这就是性能确实重要。尽管前两个步骤( Bytes
Remainder )可能是一个可以接受的折衷方案:它们确实将性能提高了大约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 = []bytes("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

甚至更好:

const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

现在这已经是一个很大的改进:我们可以实现为a

const
(有
string
常量,但没有切片常量)。作为额外的收获,表达式
len(letters)
也将是
const
!(
len(s)
如果
s
为字符串常量,则表达式为常量。)

而且要花多少钱?没事

string
可以对s进行索引,从而对其字节进行索引,这正是我们想要的。

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

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位数字的概率均相等)。尽管由于字母的数量
52
远小于
1<<63- 1
,所以失真非常小,所以在实践中这是完全可以的。

为了使这一点更容易理解:假设您想要一个范围为的随机数

0..5
。使用3个随机位,这将产生
0..1
比range两倍的概率
2..5
。使用5个随机位,范围内的数字
0..1
将以
6/32
概率出现,范围内的数字
2..5
将以
5/32
概率出现,现在更接近所需值。增加位数会使此意义降低,当达到63位时,可以忽略不计。

4.遮罩

在前面的解决方案的基础上,我们可以通过使用与代表字母数量所需的数量一样多的随机数最低位来维持字母的均等分布。因此,例如,如果我们有52个字母,则需要6位才能表示它:`52

110100b

。因此,我们将仅使用所返回数字的最低6位
rand.Int63()
。为了保持字母的均等分布,我们仅在数字落入范围内时才“接受”该数字
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.改进了遮罩

先前的解决方案仅使用由返回的63个随机位中的最低6位

rand.Int63()
。这是浪费,因为获取随机位是我们算法中最慢的部分。

如果我们有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.来源

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

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

有一个

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

因此,让我们坚持下去

math/rand
。在
rand.Rand
使用
rand.Source
作为随机比特的源。
rand.Source
是一个接口,它指定一种
Int63()int64
方法:正是我们最新解决方案中需要和使用的唯一方法。

因此,我们实际上并不需要

rand.Rand
(包的显式或全局共享
rand
),
rand.Source
对于我们来说,a 足够了:

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
状态包doc :

默认Source是安全的,可以供多个goroutine并发使用。

因此,默认来源比

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

7.利用
strings.Builder

之前的所有解决方案都返回一个,

string
其内容首先构建在切片中(
[]rune
Genesis
中以及
[]byte
后续的解决方案中),然后转换为
string
。由于
string
值是不可变的,因此最终的转换必须复制切片的内容,并且如果转换不能复制,则不能保证字符串的内容不会通过其原始切片进行修改。

Go 1.10推出

strings.Builder

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

因此,我们的下一个想法是不要在切片中构建随机字符串,而是借助a

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

请注意,在创建new之后

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

8.“模仿”
strings.Builder
包装
unsafe

strings.Builder``[]byte
就像我们自己一样,在内部构建字符串。因此,基本上通过a进行操作会
strings.Builder
产生一些开销,我们切换到的唯一
strings.Builder
方法是避免切片的最终复制。

strings.Builder
通过使用package避免最终副本
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
字节四舍五入。我们可以计算出随机索引不可用的可能性(请参见上文),因此我们可以请求更多的“可能”足够(如果事实并非如此,则重复该过程)。例如,我们可以将字节片处理为“位流”,为此,我们有一个不错的第三方lib
:(
github.com/icza/bitio
公开:我是作者)。

但是基准代码仍然表明我们没有赢。为什么会这样呢?

最后一个问题的答案是因为

rand.Read()
使用循环并不断调用,
Source.Int63()
直到它填充了传递的切片为止。
RandStringBytesMaskImprSrc()
解决方案的确切功能是,
没有
中间缓冲区,也没有增加复杂性。这就是为什么
RandStringBytesMaskImprSrc()
继续保持王位。是的,
RandStringBytesMaskImprSrc()
使用了不同步的
rand.Source
不同于
rand.Read()
。但是推理仍然适用。如果我们使用
Rand.Read()
代替
rand.Read()
(前者也是不同步的),则证明了这一点。

二。基准测试

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

关键时刻:

BenchmarkRunes-4          2000000    723 ns/op   96 B/op   2 allocs/opBenchmarkBytes-4          3000000    550 ns/op   32 B/op   2 allocs/opBenchmarkBytesRmndr-4     3000000    438 ns/op   32 B/op   2 allocs/opBenchmarkBytesMask-4      3000000    534 ns/op   32 B/op   2 allocs/opBenchmarkBytesMaskImpr-4 10000000    176 ns/op   32 B/op   2 allocs/opBenchmarkBytesMaskImprSrc-4         10000000    139 ns/op   32 B/op   2 allocs/opBenchmarkBytesMaskImprSrcSB-4       10000000    134 ns/op   16 B/op   1 allocs/opBenchmarkBytesMaskImprSrcUnsafe-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%
的内存使用和分配的减少!真好!

最后,如果我们敢于使用package

unsafe
而不是
strings.Builder
,我们将再次获得 14%的 收益。

最后进行比较来对初始解:

RandStringBytesMaskImprSrcUnsafe()
快6.3倍
RandStringRunes()
,使用 六分之一 存储器和 半尽可能少的分配 。任务完成。



转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/376041.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号