在Go语言中,我只需要一个随机的字符串(大写或小写),没有数字。最快和最简单的方法是什么?
在Go语言中,我只需要一个随机的字符串(大写或小写),没有数字。最快和最简单的方法是什么?
Answers:
Paul的解决方案提供了一个简单的通用解决方案。
问题要求“最快,最简单的方法”。让我们也讨论最快的部分。我们将以迭代的方式得出最终的最快的代码。对每个迭代进行基准测试可以在答案的结尾处找到。
所有解决方案和基准测试代码都可以在Go Playground上找到。Playground上的代码是测试文件,而不是可执行文件。您必须将其保存到一个名为的文件中XX_test.go
,然后使用
go test -bench . -benchmem
前言:
如果只需要随机字符串,最快的解决方案不是首选解决方案。为此,保罗的解决方案是完美的。这就是性能确实重要。尽管前两个步骤(Bytes和Remainder)可能是一个可以接受的折衷方案:它们确实将性能提高了大约50%(请参阅II。Benchmark部分中的确切数字),并且不会显着增加复杂性。
话虽如此,即使您不需要最快的解决方案,通读此答案也可能是冒险和有益的。
提醒一下,我们正在改进的原始通用解决方案是:
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)
}
如果要选择并组合随机字符串的字符仅包含英文字母的大写和小写字母,则只能使用字节,因为英文字母映射为UTF-8编码中的1-to-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)
}
以前的解决方案得到一个随机数,通过调用来指定一个随机的信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位时,可以忽略不计。
在前面的解决方案的基础上,我们可以通过使用与代表字母数量所需的数量一样多的随机数最低位来维持字母的均等分布。因此,例如,如果我们有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)
}
先前的解决方案仅使用由返回的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)
}
改进的遮罩效果非常好,我们可以对其进行改进。我们可以,但不值得如此复杂。
现在让我们找到其他需要改进的地方。随机数的来源。
有一个 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
状态:
默认的Source对于多个goroutine可以安全地同时使用。
因此,默认来源比Source
可能获得的慢rand.NewSource()
,因为默认来源必须在并发访问/使用下提供安全性,而rand.NewSource()
不能提供(因此Source
返回的安全性可能会更快)。
strings.Builder
先前的所有解决方案都返回一个,string
其内容首先构建在切片中([]rune
在Genesis和[]byte
后续解决方案中),然后转换为string
。由于string
值是不可变的,因此最终的转换必须复制切片的内容,并且如果转换不能复制,则不能保证不通过其原始切片修改字符串的内容。有关详细信息,请参见如何将utf8字符串转换为[] byte?和golang:[] byte(string)与[] byte(* 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()
方法,确保它分配了足够大的内部切片(以避免在添加随机字母时重新分配)。
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))
}
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.Source
不同于rand.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%的内存使用和分配的减少!真好!
最后,如果我们敢于使用package unsafe
而不是strings.Builder
,我们将再次获得14%的收益。
最后进行比较来对初始解:RandStringBytesMaskImprSrcUnsafe()
是快6.3倍比RandStringRunes()
,使用六分之一存储器和半尽可能少的分配。任务完成。
rand.Source
。更好的解决方法是将a传递rand.Source
给RandStringBytesMaskImprSrc()
函数,这样就不需要锁定,因此不会影响性能/效率。每个goroutine可以有自己的Source
。
defer
当明显不需要它时,应避免使用它。见grokbase.com/t/gg/golang-nuts/158zz5p42w/...
defer
解锁一个互斥体可以立即之前或之后调用锁是IMO 大多是一个非常好的主意; 确保您不仅可以忘记解锁,而且即使在非致命的紧急中功能时也可以解锁。
您可以为此编写代码。如果要以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))
}
rand.Seed(time.Now().Unix())
或rand.Seed(time.Now().UnixNano())
math/rand
; 使用crypto/rand
(例如@Not_A_Golfer的选项1)代替。
两种可能的选择(当然可能还有更多选择):
您可以使用crypto/rand
支持读取随机字节数组(从/ dev / urandom)并适合于加密随机生成的软件包。参见http://golang.org/pkg/crypto/rand/#example_Read。但是,它可能比正常的伪随机数生成慢。
取一个随机数,然后使用md5或类似的方式对其进行哈希处理。
在详细icza's
解释了解决方案之后,这里是对它的crypto/rand
替代,使用的修改math/rand
。
const (
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" // 52 possibilities
letterIdxBits = 6 // 6 bits to represent 64 possibilities / indexes
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
)
func SecureRandomAlphaString(length int) string {
result := make([]byte, length)
bufferSize := int(float64(length)*1.3)
for i, j, randomBytes := 0, 0, []byte{}; i < length; j++ {
if j%bufferSize == 0 {
randomBytes = SecureRandomBytes(bufferSize)
}
if idx := int(randomBytes[j%length] & letterIdxMask); idx < len(letterBytes) {
result[i] = letterBytes[idx]
i++
}
}
return string(result)
}
// SecureRandomBytes returns the requested number of bytes using crypto/rand
func SecureRandomBytes(length int) []byte {
var randomBytes = make([]byte, length)
_, err := rand.Read(randomBytes)
if err != nil {
log.Fatal("Unable to generate random bytes")
}
return randomBytes
}
如果您想要一个更通用的解决方案,该解决方案允许您传入字符字节片以创建字符串,则可以尝试使用以下方法:
// SecureRandomString returns a string of the requested length,
// made from the byte characters provided (only ASCII allowed).
// Uses crypto/rand for security. Will panic if len(availableCharBytes) > 256.
func SecureRandomString(availableCharBytes string, length int) string {
// Compute bitMask
availableCharLength := len(availableCharBytes)
if availableCharLength == 0 || availableCharLength > 256 {
panic("availableCharBytes length must be greater than 0 and less than or equal to 256")
}
var bitLength byte
var bitMask byte
for bits := availableCharLength - 1; bits != 0; {
bits = bits >> 1
bitLength++
}
bitMask = 1<<bitLength - 1
// Compute bufferSize
bufferSize := length + length / 3
// Create random string
result := make([]byte, length)
for i, j, randomBytes := 0, 0, []byte{}; i < length; j++ {
if j%bufferSize == 0 {
// Random byte buffer is empty, get a new one
randomBytes = SecureRandomBytes(bufferSize)
}
// Mask bytes to get an index into the character slice
if idx := int(randomBytes[j%length] & bitMask); idx < availableCharLength {
result[i] = availableCharBytes[idx]
i++
}
}
return string(result)
}
如果您想传递自己的随机性来源,则修改上述内容以接受io.Reader
而不是使用会很简单crypto/rand
。
如果您希望密码安全随机数,并且确切的字符集是灵活的(例如,base64很好),则可以根据所需的输出大小来精确计算所需的随机字符长度。
基数64的文本比基数256长1/3。(2 ^ 8与2 ^ 6; 8位/ 6位= 1.333的比率)
import (
"crypto/rand"
"encoding/base64"
"math"
)
func randomBase64String(l int) string {
buff := make([]byte, int(math.Round(float64(l)/float64(1.33333333333))))
rand.Read(buff)
str := base64.RawURLEncoding.EncodeToString(buff)
return str[:l] // strip 1 extra character we get from odd length results
}
注意:如果您喜欢+和/字符而不是-和_,也可以使用RawStdEncoding
如果要使用十六进制,则基数16比基数256长2倍。(2 ^ 8与2 ^ 4; 8bits / 4bits = 2x ratio)
import (
"crypto/rand"
"encoding/hex"
"math"
)
func randomBase16String(l int) string {
buff := make([]byte, int(math.Round(float64(l)/2)))
rand.Read(buff)
str := hex.EncodeToString(buff)
return str[:l] // strip 1 extra character we get from odd length results
}
但是,如果您的字符集具有从base256到baseN的编码器,则可以将其扩展到任意字符集。您可以使用相同的大小进行计算,以表示字符集需要多少位。任何任意字符集的比率计算为:)ratio = 8 / log2(len(charset))
。
尽管这两种解决方案都是安全,简单,应该快速的,并且不会浪费您的加密熵池。
这是操场显示它适用于任何大小。https://play.golang.org/p/i61WUVR8_3Z
func Rand(n int) (str string) {
b := make([]byte, n)
rand.Read(b)
str = fmt.Sprintf("%x", b)
return
}
[]byte
?
如果您愿意在允许的字符池中添加一些字符,则可以使该代码与通过io.Reader提供随机字节的任何内容一起使用。在这里我们正在使用crypto/rand
。
// len(encodeURL) == 64. This allows (x <= 265) x % 64 to have an even
// distribution.
const encodeURL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
// A helper function create and fill a slice of length n with characters from
// a-zA-Z0-9_-. It panics if there are any problems getting random bytes.
func RandAsciiBytes(n int) []byte {
output := make([]byte, n)
// We will take n bytes, one byte for each character of output.
randomness := make([]byte, n)
// read all random
_, err := rand.Read(randomness)
if err != nil {
panic(err)
}
// fill output
for pos := range output {
// get random item
random := uint8(randomness[pos])
// random % 64
randomPos := random % uint8(len(encodeURL))
// put into output
output[pos] = encodeURL[randomPos]
}
return output
}
random % 64
必要?
len(encodeURL) == 64
。如果random % 64
未完成,则randomPos
可能会> = 64,并在运行时引起紧急恐慌。
const (
chars = "0123456789_abcdefghijkl-mnopqrstuvwxyz" //ABCDEFGHIJKLMNOPQRSTUVWXYZ
charsLen = len(chars)
mask = 1<<6 - 1
)
var rng = rand.NewSource(time.Now().UnixNano())
// RandStr 返回指定长度的随机字符串
func RandStr(ln int) string {
/* chars 38个字符
* rng.Int63() 每次产出64bit的随机数,每次我们使用6bit(2^6=64) 可以使用10次
*/
buf := make([]byte, ln)
for idx, cache, remain := ln-1, rng.Int63(), 10; idx >= 0; {
if remain == 0 {
cache, remain = rng.Int63(), 10
}
buf[idx] = chars[int(cache&mask)%charsLen]
cache >>= 6
remain--
idx--
}
return *(*string)(unsafe.Pointer(&buf))
}
BenchmarkRandStr16-8 20000000 68.1 ns / op 16 B / op 1 allocs / op