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


当前回答

保罗的解决方案提供了一个简单而普遍的解决方案。

这个问题问的是“最快最简单的方法”。让我们也谈谈最快的部分。我们将以迭代的方式得到最终的、最快的代码。每次迭代的基准测试可以在答案的末尾找到。

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

go test -bench . -benchmem

前言:

如果您只需要一个随机字符串,那么最快的解决方案并不是最佳解决方案。为此,保罗的解决方案是完美的。这是在性能确实重要的情况下。虽然前两步(字节和余数)可能是一个可以接受的折衷:它们确实提高了50%的性能(具体数字见II。基准测试部分),并且它们不会显著增加复杂性。

话虽如此,即使你不需要最快的解决方案,通读这个答案可能是冒险的,也是有教育意义的。

我改进

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 = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

或者更好:

const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

现在这已经是一个很大的改进:我们可以实现它是一个const(有字符串常量,但没有切片常量)。作为额外的增益,表达式len(letters)也将是一个const!(如果s是一个字符串常量,则表达式len(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.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的概率是2..5的两倍。使用5个随机位,范围为0..1将以6/32的概率出现,数字在范围2..5个,概率是5/32,现在更接近目标了。增加比特数使其不那么重要,当达到63位时,它可以忽略不计。

4. 屏蔽

Building on the previous solution, we can maintain the equal distribution of letters by using only as many of the lowest bits of the random number as many is required to represent the number of letters. So for example if we have 52 letters, it requires 6 bits to represent it: 52 = 110100b. So we will only use the lowest 6 bits of the number returned by rand.Int63(). And to maintain equal distribution of letters, we only "accept" the number if it falls in the range 0..len(letterBytes)-1. If the lowest bits are greater, we discard it and query a new random number.

Note that the chance of the lowest bits to be greater than or equal to len(letterBytes) is less than 0.5 in general (0.25 on average), which means that even if this would be the case, repeating this "rare" case decreases the chance of not finding a good number. After n repetition, the chance that we still don't have a good index is much less than pow(0.5, n), and this is just an upper estimation. In case of 52 letters the chance that the 6 lowest bits are not good is only (64-52)/64 = 0.19; which means for example that chances to not have a good number after 10 repetition is 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. 掩蔽改进

前面的解决方案只使用rand.Int63()返回的63个随机位中最低的6位。这是一种浪费,因为获取随机位是我们算法中最慢的部分。

如果我们有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实现了一个加密安全的伪随机数生成器,所以它要慢得多。

所以让我们专注于数学/rand包。兰德。Rand使用Rand。源作为源的随机位。兰德。Source是一个指定Int63() int64方法的接口:恰好是我们在最新解决方案中需要和使用的唯一方法。

所以我们不需要兰特。兰德(无论是显式的还是全局的,共享的兰德包之一),一个兰德。来源对我们来说已经足够完美了:

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

还要注意,最后一个解决方案不需要初始化(播种)math/ Rand包的全局Rand,因为它没有被使用(以及我们的Rand。源代码已正确初始化/播种)。

这里还有一件事需要注意:math/rand状态的包文档:

默认的Source对于多个gorout例程并发使用是安全的。

因此,默认源比rand.NewSource()可能获得的source要慢,因为默认源必须在并发访问/使用下提供安全性,而rand.NewSource()不提供这一点(因此它返回的source更可能更快)。

7. 利用字符串。构建器

All previous solutions return a string whose content is first built in a slice ([]rune in Genesis, and []byte in subsequent solutions), and then converted to string. This final conversion has to make a copy of the slice's content, because string values are immutable, and if the conversion would not make a copy, it could not be guaranteed that the string's content is not modified via its original slice. For details, see How to convert utf8 string to []byte? and golang: []byte(string) vs []byte(*string).

Go 1.10 introduced strings.Builder. strings.Builder is a new type we can use to build contents of a string similar to bytes.Buffer. Internally it uses a []byte to build the content, and when we're done, we can obtain the final string value using its Builder.String() method. But what's cool in it is that it does this without performing the copy we just talked about above. It dares to do so because the byte slice used to build the string's content is not exposed, so it is guaranteed that no one can modify it unintentionally or maliciously to alter the produced "immutable" string.

我们的下一个想法不是在切片中构建随机字符串,而是在字符串的帮助下。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()
}

请注意,在创建新字符串后。builder,我们调用它的Builder.Grow()方法,确保它分配了一个足够大的内部片(以避免在添加随机字母时重新分配)。

8. “模仿”字符串。包装不安全的施工人员

字符串。生成器在内部[]字节中构建字符串,与我们自己所做的一样。基本上是通过一个字符串。Builder有一些开销,我们唯一切换到字符串的东西。构建器的目的是避免片的最终复制。

字符串。生成器通过使用包不安全避免最终复制:

// String returns the accumulated string.
func (b *Builder) String() string {
    return *(*string)(unsafe.Pointer(&b.buf))
}

问题是,我们自己也可以这样做。因此,这里的想法是切换回在[]字节中构建随机字符串,但当我们完成后,不要将其转换为返回的字符串,而是进行不安全的转换:获得一个指向字节切片的字符串作为字符串数据。

以下是具体的做法:

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字节)。但在这一点上,我们已经做得更糟了(因为获得随机比特是“困难的部分”),我们得到的比需要的更多。

还要注意,为了保持所有字母索引的均匀分布,可能会有一些我们不能使用的“垃圾”随机数据,因此我们最终会跳过一些数据,从而在遍历所有字节片时结束。我们需要进一步“递归地”获得更多的随机字节。现在我们甚至失去了“单次呼叫兰德包”的优势……

We could "somewhat" optimize the usage of the random data we acquire from math.Rand(). We may estimate how many bytes (bits) we'll need. 1 letter requires letterIdxBits bits, and we need n letters, so we need n * letterIdxBits / 8.0 bytes rounding up. We can calculate the probability of a random index not being usable (see above), so we could request more that will "more likely" be enough (if it turns out it's not, we repeat the process). We can process the byte slice as a "bit stream" for example, for which we have a nice 3rd party lib: github.com/icza/bitio (disclosure: I'm the author).

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

The answer to the last question is because rand.Read() uses a loop and keeps calling Source.Int63() until it fills the passed slice. Exactly what the RandStringBytesMaskImprSrc() solution does, without the intermediate buffer, and without the added complexity. That's why RandStringBytesMaskImprSrc() remains on the throne. Yes, RandStringBytesMaskImprSrc() uses an unsynchronized rand.Source unlike rand.Read(). But the reasoning still applies; and which is proven if we use Rand.Read() instead of rand.Read() (the former is also unsynchronzed).

2基准

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

关键时刻:

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。兰德,我们再次获得21%的收益。

如果我们使用字符串。生成器,我们获得了微小的3.5%的速度,但我们也实现了50%的内存使用和分配减少!那很好啊!

最后,如果我们敢于使用包不安全而不是字符串。建筑商,我们又获得了14%的收益。

将最终解决方案与初始解决方案进行比较:RandStringBytesMaskImprSrcUnsafe()比randstringgrunes()快6.3倍,使用六分之一的内存和一半的分配。任务完成

其他回答

另一个版本,灵感来自于生成密码在JavaScript加密:

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

你可以为它编写代码。如果您希望使用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))
}

这里是一个简单而高效的加密安全随机字符串的解决方案。

package main

import (
    "crypto/rand"
    "unsafe"
    "fmt"
)

var alphabet = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

func main() {
    fmt.Println(generate(16))
}

func generate(size int) string {
    b := make([]byte, size)
    rand.Read(b)
    for i := 0; i < size; i++ {
        b[i] = alphabet[b[i] % byte(len(alphabet))]
    }
    return *(*string)(unsafe.Pointer(&b))
}

基准

Benchmark  95.2 ns/op      16 B/op      1 allocs/op

如果您想要加密安全的随机数,并且确切的字符集是灵活的(例如,base64就可以),那么您可以根据所需的输出大小精确计算所需的随机字符的长度。

64进制文本比256进制文本长1/3。(2^8 vs 2^6;8位/6位= 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
}

注意:你也可以使用RawStdEncoding,如果你喜欢+和/字符-和_

如果你想要十六进制,以16为底比以256为底长2倍。(2^8 vs 2^4;8位/4位= 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
}

但是,如果您的字符集使用base256到baseN编码器,则可以将其扩展到任何任意字符集。你可以用同样的大小计算需要多少位来表示你的字符集。任何任意字符集的比率计算是:ratio = 8 / log2(len(字符集)))。

虽然这两种解决方案都是安全的,简单的,应该是快速的,并且不会浪费您的加密熵池。

这是一个游乐场,它适用于任何尺寸。https://play.golang.org/p/_yF_xxXer0Z

两个可能的选项(当然可能有更多):

您可以使用crypto/rand包,它支持读取随机字节数组(来自/dev/urandom),并面向加密随机生成。见http://golang.org/pkg/crypto/rand/#example_Read。它可能比普通的伪随机数生成慢。 取一个随机数,用md5或类似的方法进行哈希。