如何在golang中生成一个固定长度的随机string?
我只想要一个随机的string(大写或小写),在Golang中没有数字。 Go的最快最简单的方法是什么?
Paul的解决scheme提供了一个简单的通用解决scheme
这个问题要求“最快最简单的方法” 。 我们来解决这个问题。 我们将以迭代的方式到达最终的最快代码。 基准每个迭代可以在答案的最后find。
所有的解决scheme和基准代码可以在Go游乐场find。 Playground上的代码是一个testing文件,而不是一个可执行文件。 您必须将其保存到名为XX_test.go
的文件中,并使用go test -bench .
运行它go test -bench .
。
一,改进
1.创世纪(Runes)
提醒一下,我们正在改进的最初的一般解决scheme是:
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.字节
如果从中select和组装随机string的字符只包含英文字母的大写和小写字母,那么我们只能处理字节,因为英文字母字符映射到UTF-8编码中的字节1到1(其中Go是如何存储string的)。
所以,而不是:
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
我们可以用:
var letters = []bytes("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
甚至更好:
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
现在这已经是一个很大的改进了:我们可以把它做成一个const
(有string
常量,但没有切片常量 )。 作为一个额外的收益,expression式len(letters)
也将是一个const
! (如果s
是一个string常量,则expression式len(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) }
剩下的
以前的解决scheme通过调用rand.Intn()
来获得一个随机数,指定一个随机字母, 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位数字)。 尽pipe由于字母52
的数量比1<<63 - 1
小得多,因此失真非常小,所以实际上这是非常好的。
为了使这一点更容易理解:假设你想要一个在0..5
范围内的随机数。 使用3个随机位,这将产生两倍于概率0..1
的数字0..1
。 使用5个随机位,范围0..1
数字将以6/32
概率出现,范围2..5
数字以5/32
概率出现,现在更接近所需的数字。 增加位数使得这一点不太重要,达到63位时,可以忽略不计。
4.掩蔽
在以前的解决scheme的基础上,我们可以通过仅使用尽可能多的随机数的最低位来表示字母数量来维持字母的平均分配。 例如,如果我们有52个字母,则需要6位来表示它: 52 = 110100b
。 所以我们只使用由rand.Int63()
返回的数字的最低6位。 为了保持字母的平均分配,如果数字落在0..len(letterBytes)-1
范围内,我们只接受这个数字。 如果最低位比较大,我们丢弃它并查询一个新的随机数。
请注意,最低位大于或等于len(letterBytes)
通常小于0.5
(平均0.25
),这意味着即使情况如此,重复此“罕见”情况也会降低没有find好的号码的机会。 经过n
次重复,我们基本没有好的指数的机会远远小于pow(0.5, n)
,这只是一个上限估计。 在52个字母的情况下,6个最低位不好的可能性只有(64-52)/64 = 0.19
; 这意味着例如在10次重复之后没有好数字的机会是1e-8
。
所以这里是解决scheme:
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.掩蔽改进
以前的解决scheme只使用rand.Int63()
返回的63个随机位中的最低6位。 这是浪费,因为获取随机比特是我们algorithm中最慢的部分。
如果我们有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非常好,我们可以改进的不多。 我们可以,但不值得复杂。
现在让我们find其他的东西来改善。 随机数的来源。
有一个crypto/rand
包提供了一个Read(b []byte)
函数,所以我们可以用它来获取尽可能多的字节。 这在性能方面没有帮助,因为crypto/rand
实现了一个密码安全的伪随机数发生器,所以速度要慢很多。
所以让我们坚持math/rand
包。 rand.Rand
使用rand.Source
作为随机位的来源。 rand.Source
是一个指定Int63() int64
方法的接口:完全是我们最新解决scheme中唯一需要和使用的方法。
所以我们并不需要一个rand.Rand
(显式的或全局的,共享一个rand
包),一个rand.Source
对我们来说已经足够了:
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) }
还要注意,这个最后的解决scheme不需要你初始化(种子) math/rand
包的全局Rand
,因为这是不使用(和我们的rand.Source
被正确初始化/种子)。
还有一件事要注意: math/rand
包doc:
默认的Source对于多个goroutine并发使用是安全的。
所以默认的源比rand.NewSource()
可能获得的源更慢,因为默认源必须在并发访问/使用下提供安全性,而rand.NewSource()
不提供这个(因此Source
返回由它更有可能更快)。
II。 基准
好吧,让我们来衡量一下不同的解决scheme。
BenchmarkRunes 1000000 1703 ns/op BenchmarkBytes 1000000 1328 ns/op BenchmarkBytesRmndr 1000000 1012 ns/op BenchmarkBytesMask 1000000 1214 ns/op BenchmarkBytesMaskImpr 5000000 395 ns/op BenchmarkBytesMaskImprSrc 5000000 303 ns/op
只需从符号切换到字节,我们立即有22%的性能增益。
摆脱rand.Intn()
和使用rand.Int63()
而不是提供了另一个24%的提升。
掩蔽(并在重大索引的情况下重复)减慢一点(由于重复呼叫):- 20% …
但是,当我们使用63个随机比特(一个rand.Int63()
调用中的10个索引)中的全部(或大部分)时:加速了3.4倍 。
最后,如果我们使用(非默认的,新的) rand.Source
而不是rand.Rand
,我们再次获得23%的收益。
比较最终的解决scheme: RandStringBytesMaskImprSrc()
比RandStringRunes()
快5.6倍 。
你可以为它编写代码。 这个代码可以更简单一些,如果你想要用UTF-8编码所有字母都是单个字节的话。
package main import ( "fmt" "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() { fmt.Println(randSeq(10)) }
两种可能的select(当然可能更多):
-
您可以使用支持读取随机字节数组的
crypto/rand
软件包(来自/ dev / urandom),并且适用于密码随机生成。 请参阅http://golang.org/pkg/crypto/rand/#example_Read 。 尽pipe如此,它可能比正常的伪随机数生成要慢。 -
采取一个随机数字,并使用MD5或类似的东西哈希它。
使用uniuri软件包,它生成密码安全的统一(无偏)string。
以下icza's
奇妙解释的解决scheme,这是一个修改,它使用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 }
如果你想要一个更通用的解决scheme,它允许你传递字符字节片来创buildstring,你可以尝试使用这个:
// 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
是微不足道的。
此外,我发现了一个包含一堆方法来操纵假数据的包。 在开发https://github.com/Pallinder/go-randomdata时发现它对于播种数据库很有用。; 也许对别人有帮助
如果你愿意在允许的字符池中添加几个字符,你可以使用任何通过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 }