假如我们要生成一个固定长度的随机字符串,包含大小写字母,没有数字,没有特殊字符串,那么我们怎么做呢?需要怎样优化,才会更简单,更高效?在最终的方案之前,我们看看最常见的写法是怎样的,然后是如何一步步演进到最终的高效率方案的。好吧,先看下最原始的方案。

常见做法(Runes)

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

这个实现比较简单,二十六字母(大小写),然后随机取数,获得随机字符串。

Bytes改进

runebytebyteuint8runeint32
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)
}
runeletterlen(letterBytes)

余数改进

rand.Intn()rand.Intn()Rand.Intn()Rand.Intn()Rand.Int31n()rand.Int63()rand.Intn()
rand.Intn()rand.Int63()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)
}
1<<63-1

Masking 掩码

52==110100brand.Int63()len(letterBytes)-1
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
    letterIdxBits = 6                    // 6 bits to represent a letter index
    letterIdxMask = 1<1 // All 1-bits, as many as letterIdxBits
)func RandStringBytesMask(n int) string {
    b := make([]byte, n)for i := 0; i         if idx := int(rand.Int63() & letterIdxMask); idx len(letterBytes) {
            b[i] = letterBytes[idx]
            i++
        }
    }return string(b)
}
(64-52)/64 = 0.191e-8

Masking 掩码改进

rand.Int63()rand.Int63()63/6=10
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
    letterIdxBits = 6                    // 6 bits to represent a letter index
    letterIdxMask = 1<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)
}
rand.Int63()

rand Source 优化

rand.Randrand.Sourcerand.Sourcefunc Int63() int64
// A Source represents a source of uniformly-distributed
// pseudo-random int64 values in the range [0, 1<<63).
type Source interface {
    Int63() int64
    Seed(seed 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)
}
rand.Int63()randsrc

strings.Builder 改进

这个是G0 1.10 新增的功能,提升字符串拼接的效率,这方面的可以参考我以前写的三篇文章,这里不做过多的介绍了。

Go语言字符串高效拼接(一)

Go语言字符串高效拼接(二)

Go语言字符串高效拼接(三)

经过改进后,代码如下:

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

使用unsafe包模拟 strings.Builder

strings.Builder[]bytestringunsafe
// 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))
}
strings.Builder

Benchmark 性能测试

以后,我们通过一步步的改进代码,提升效率,现在我们通过Benchmark测试看下这些方法的效果。

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
runebyte
rand.Int63rand.Intn

单纯的使用掩码,因为重复获取可用索引的问题,性能下降了** -22%**。

但是当我们对 Masking 掩码 进行改进,分为10部分缓存的时候,我们获得了3倍的提升。

使用rand.Source 代替 rand.Rand, 我们再次获得了21%的提升。

strings.Builder
unsafestrings.Builder
RandStringBytesMaskImprSrcUnsafeRandStringRunes

结语

这是一篇stackoverflow的文章,有人提问 How to generate a random string of a fixed length in Go? ,icza 做了非常精彩的回答,我把整个翻译下来加以整理分享给大家。

这是一篇非常棒的文章,它的意义不光是回答这个问题,还有帮助我们建立如何一步步优化的思路以及追求极致的极客精神。

与大家共勉。