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

Ani*_*hah 270 string random go

我想在Go中只有一个随机字符串(大写或小写),没有数字.什么是最快最简单的方法?

icz*_*cza 704

Paul的解决方案提供了一种简单,通用的解决方案.

这个问题要求"最快最简单的方法".我们来解决这个问题.我们将以迭代的方式获得最终,最快的代码.可以在答案的最后找到每次迭代的基准测试.

所有解决方案和基准测试代码都可以在Go Playground上找到.Playground上的代码是测试文件,而不是可执行文件.您必须将其保存到一个名为的文件中XX_test.go并使用它运行它const.

一,改进

1.创世纪(符文)

提醒一下,我们正在改进的原始通用解决方案是:

go test -bench . -benchmem
Run Code Online (Sandbox Code Playgroud)

2.字节

如果要选择的字符和汇编随机字符串只包含英文字母的大写和小写字母,我们只能使用字节,因为英文字母字母映射到UTF-8编码中的字节1对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)
}
Run Code Online (Sandbox Code Playgroud)

我们可以用:

var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
Run Code Online (Sandbox Code Playgroud)

甚至更好:

var letters = []bytes("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
Run Code Online (Sandbox Code Playgroud)

现在这已经是一个很大的改进:我们可以实现它string(有len(letters)常量,但没有切片常量).作为一个额外的收获,表达const也将是一个len(s)!(s如果string是字符串常量,则表达式是常量.)

费用是多少?什么都没有.rand.Intn()s可以索引索引其字节,完美,正是我们想要的.

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

const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
Run Code Online (Sandbox Code Playgroud)

3.剩余

以前的解决方案获得一个随机数,通过调用Rand.Intn()哪个委托给Rand.Int31n()哪个委托来指定随机字母rand.Int63().

rand.Int63()产生具有63个随机位的随机数相比,这要慢得多.

所以我们可以简单地调用len(letterBytes)并使用除以下之后的余数rand.Int63():

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)
}
Run Code Online (Sandbox Code Playgroud)

这种方法效果明显更快,缺点是所有字母的概率都不完全相同(假设52所有63位数字的概率相等).虽然失真非常小,因为字母的数量1<<63 - 1远远小于0..5,所以在实践中这是完全正常的.

为了使这个理解更容易:假设你想要一个范围内的随机数0..1.使用3个随机位,这将产生2..5具有双倍概率的数字而不是范围0..1.使用5个随机比特,范围内的数字6/32将出现2..5概率,数字在范围内5/32,52 = 110100b概率现在更接近期望值.增加位数会使其不那么重要,当达到63位时,它可以忽略不计.

4.掩蔽

在前面的解决方案的基础上,我们可以通过使用随机数的最低位来保持字母的均等分布,因为需要许多字母来表示字母数.因此,例如,如果我们有52个字母,则需要6位来表示它:rand.Int63().所以我们只使用返回的最低6位数0..len(letterBytes)-1.为了保持字母的平等分配,如果数字落在范围内,我们只会"接受"该数字len(letterBytes).如果最低位更大,我们将其丢弃并查询新的随机数.

请注意,最低位大于或等于的概率0.5小于0.25一般(n平均值),这意味着即使是这种情况,重复这种"罕见"情况也会降低找不到好处的机会数.pow(0.5, n)重复之后,我们没有良好指数的机会远小于(64-52)/64 = 0.19,这只是一个较高的估计.在52个字母的情况下,6个最低位不好的可能性仅为1e-8; 这意味着例如在10次重复之后没有好数字的机会是rand.Int63().

所以这是解决方案:

func RandStringBytesRmndr(n int) string {
    b := make([]byte, n)
    for i := range b {
        b[i] = letterBytes[rand.Int63() % int64(len(letterBytes))]
    }
    return string(b)
}
Run Code Online (Sandbox Code Playgroud)

5.掩蔽改进

先前的解决方案仅使用由返回的63个随机位中的最低6位63/6 = 10.这是一种浪费,因为获取随机位是我们算法中最慢的部分.

如果我们有52个字母,那意味着6位代码一个字母索引.所以63个随机位可以指定crypto/rand不同的字母索引.让我们使用所有这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
)

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)
}
Run Code Online (Sandbox Code Playgroud)

6.来源

改进的屏蔽是相当不错的,没有多少可以改善它.我们可以,但不值得复杂.

现在让我们找到其他改进的东西.随机数的来源.

有一个Read(b []byte)提供crypto/rand函数的包,所以我们可以使用它来通过一次调用获得尽可能多的字节数.这在性能方面没有帮助,因为它math/rand实现了加密安全的伪随机数生成器,因此速度要慢得多.

所以让我们坚持下去rand.Rand.在rand.Source使用rand.Source作为随机比特的源.Int63() int64是一个指定rand.Rand方法的接口:我们在最新的解决方案中确切地和唯一需要和使用的方法.

所以我们真的不需要rand(显式的或全局的,共享的一个rand.Source包),Rand对我们来说是完全足够的:

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)
}
Run Code Online (Sandbox Code Playgroud)

另请注意,最后一个解决方案不要求您初始化(种子)包的全局math/rand(rand.Source因为未使用)(并且我们math/rand已正确初始化/播种).

还有一点需要注意:包Source状态文件:

默认Source对于多个goroutine并发使用是安全的.

所以默认源比较慢rand.NewSource(),可能通过以下方式获得rand.NewSource(),因为默认源必须提供并发访问/使用条件下的安全性,同时Source不提供这个(因此strings.Builder返回由它更可能会更快).

(7.使用string)

Go 1.7添加了一个[]rune函数和一个[]byte方法.我们应该尝试使用它们在一个步骤中读取所需的字节数,以获得更好的性能.

这有一个小"问题":我们需要多少字节?我们可以说:输出字母数量多.我们认为这是一个较高的估计,因为字母索引使用少于8位(1字节).但是在这一点上,我们已经做得更糟(因为获得随机位是"困难部分"),而且我们得到的不仅仅是需要.

还要注意,为了保持所有字母索引的平均分配,可能会有一些我们无法使用的"垃圾"随机数据,因此我们最终会跳过一些数据,因此当我们通过所有数据时最终会缩短字节切片.我们需要进一步获得更多随机字节,"递归地".而现在我们甚至失去了"单string包电话"的优势......

我们可以"稍微"优化我们从中获取的随机数据的使用string.我们可以估计我们需要多少字节(比特).1个字母需要strings.Builder位,我们需要strings.Builder字母,所以我们需要string字节四舍五入.我们可以计算随机索引不可用的概率(参见上文),因此我们可以请求更多"更可能"足够的(如果事实证明它不是,我们重复这个过程).我们可以将字节片处理为"比特流",例如,我们有一个很好的第三方库:( bytes.Buffer披露:我是作者).

但基准代码仍显示我们没有获胜.为什么会这样?

最后一个问题的答案是因为[]byte使用循环并保持调用string直到它填充传递的切片.正是Builder.String()解决方案的作用,没有中间缓冲区,没有增加复杂性.这就是strings.Builder保持在宝座上的原因.是的,strings.Buidler使用不同步的Builder.Grow()不同strings.Builder.但推理仍然适用; 如果我们使用unsafe而不是strings.Builder(前者也是不同步的),这证明了.

II.基准

好吧,让我们对不同的解决方案进行基准测试.

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)
}
Run Code Online (Sandbox Code Playgroud)

只需从符文切换到字节,我们立即获得22%的性能提升.

摆脱[]byte和使用strings.Builder相反提高了24%.

掩蔽(并在大指数的情况下重复)减慢一点(由于重复调用):- 20% ......

但是当我们使用63个随机位中的所有(或大部分)时(一次strings.Builder调用10个索引):速度提高了3.4倍.

最后如果我们用(非默认的,新的)strings.Builder代替unsafe,我们再次获得23%.

最后相较于最初的解决方案:[]byte快5.6倍string.

  • @icza,这是我在SO上看了很长时间的最佳答案之一! (86认同)
  • @RobbieV Yup,因为使用了共享的`rand.Source`.一个更好的解决方法是将`rand.Source`传递给`RandStringBytesMaskImprSrc()`函数,这样就不需要锁定,因此不会影响性能/效率.每个goroutine都有自己的`Source`. (8认同)
  • @Flimzy 我玩了更多,但仍然无法想出一个解决方案,将丢弃的位转化为速度优势(同时保持均匀分布)。尽管重新审视了这个问题,但我确实添加了 2 个额外的解决方案来进一步提高性能(使用 `strings.Builder` 和一个“不安全”的解决方案)。 (3认同)
  • @StevenSoroka 我同意你的看法。如果您只需要一个随机字符串,最快的解决方案不是首选解决方案。为此,保罗的解决方案是完美的。这就是性能是否重要。尽管前两个步骤(“字节”和“剩余”)可能是可以接受的折衷方案:它们确实将性能提高了 50%,并且不会显着增加复杂性。 (2认同)

Pau*_*kin 121

你可以为它编写代码.如果你想在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))
}
Run Code Online (Sandbox Code Playgroud)

  • 不要忘记rand.Seed(),否则每次第一次启动都会得到相同的字符串... rand.Seed(time.Now().UTC().UnixNano()) (26认同)
  • 对于一个难以猜测的秘密 - 密码,加密密钥等 - 从不使用`math/rand`; 使用`crypto/rand`(比如@ Not_A_Golfer的选项1). (7认同)
  • 请注意,如果您正在使用种子尝试上述程序,那么在游乐场中,它将始终返回相同的结果.我在操场上试了一下,经过一段时间才意识到这一点.对我来说它工作得很好.希望它能节省时间:) (3认同)
  • Evan的添加是正确的,但是还有其他类似的选项:`rand.Seed(time.Now().Unix())`或`rand.Seed(time.Now().UnixNano())` (2认同)

Ami*_*aei 31

为您提供简单的解决方案,重复结果最少:

import (
    "fmt"
    "math/rand"
    "time"
)

func randomString(length int) string {
    rand.Seed(time.Now().UnixNano())
    b := make([]byte, length+2)
    rand.Read(b)
    return fmt.Sprintf("%x", b)[2 : length+2]
}
Run Code Online (Sandbox Code Playgroud)

在PlayGround中查看

  • 不要每次通话都播种!您应该在程序启动时播种一次。 (2认同)

Not*_*fer 16

两种可能的选择(当然可能还有更多):

  1. 您可以使用crypto/rand支持读取随机字节数组的包(来自/ dev/urandom),并且适用于加密随机生成.请参阅http://golang.org/pkg/crypto/rand/#example_Read.但它可能比正常的伪随机数生成慢.

  2. 取一个随机数并使用md5或类似的东西哈希.


dch*_*est 13

使用package uniuri,它生成加密安全的统一(无偏)字符串.

  • 谢谢你。虽然我很欣赏其他答案中的充足细节,但您的答案提供了一种快速实用的方法来生成我想要的短随机字符串。 (3认同)

Nin*_*ham 12

另一个版本,灵感来自JavaScript crypto 中的生成密码

package main

import (
    "crypto/rand"
    "fmt"
)

var chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-"

func shortID(length int) string {
    ll := len(chars)
    b := make([]byte, length)
    rand.Read(b) // generates len(b) random bytes
    for i := 0; i < length; i++ {
        b[i] = chars[int(b[i])%ll]
    }
    return string(b)
}

func main() {
    fmt.Println(shortID(18))
    fmt.Println(shortID(18))
    fmt.Println(shortID(18))
}
Run Code Online (Sandbox Code Playgroud)


Ste*_*oka 6

如果您想要加密安全的随机数,并且确切的字符集是灵活的(例如,base64 很好),您可以从所需的输出大小中准确计算出您需要的随机字符的长度。

Base 64 文本比 base 256 长 1/3。(2^8 vs 2^6;8bits/6bits = 1.333 比率)

import (
    "crypto/rand"
    "encoding/base64"
    "math"
)

func randomBase64String(l int) string {
    buff := make([]byte, int(math.Ceil(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
}
Run Code Online (Sandbox Code Playgroud)

注意:如果您更喜欢 + 和 / 字符而不是 - 和 _,您也可以使用 RawStdEncoding

如果你想要十六进制,基数 16 比基数 256 长 2 倍。(2^8 对 2^4;8bits/4bits = 2x 比率)

import (
    "crypto/rand"
    "encoding/hex"
    "math"
)


func randomBase16String(l int) string {
    buff := make([]byte, int(math.Ceil(float64(l)/2)))
    rand.Read(buff)
    str := hex.EncodeToString(buff)
    return str[:l] // strip 1 extra character we get from odd length results
}
Run Code Online (Sandbox Code Playgroud)

但是,如果您的字符集有 base256 到 baseN 编码器,则可以将其扩展到任何任意字符集。您可以使用表示字符集所需的位数进行相同的大小计算。任何任意字符集的比率计算是:) ratio = 8 / log2(len(charset))

尽管这两种解决方案都安全、简单、应该很快,并且不会浪费您的加密熵池。

这是显示它适用于任何尺寸的操场。https://play.golang.org/p/_yF_xxXer0Z


Chr*_*ris 5

遵循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
}
Run Code Online (Sandbox Code Playgroud)

如果您想要一个更通用的解决方案,允许您传入字符字节片段以从中创建字符串,您可以尝试使用以下方法:

// 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)
}
Run Code Online (Sandbox Code Playgroud)

如果你想传入你自己的随机源,修改上面的代码以接受 anio.Reader而不是使用crypto/rand.


Dim*_*ima 5

这是我的方法)根据需要使用数学兰特或加密兰特。

func randStr(len int) string {
    buff := make([]byte, len)
    rand.Read(buff)
    str := base64.StdEncoding.EncodeToString(buff)
    // Base 64 can be longer than len
    return str[:len]
}
Run Code Online (Sandbox Code Playgroud)