如果想在Go語言中生成隻有隨機字符(大寫或小寫)、沒有數字的定長字符串。什麽方法最快最簡單?

這個問題要求“最快最簡單的方法”。我們將由淺入深,以循序漸進的方式給出最終最快的代碼。(可以在答案的最後找到每次迭代的基準測試。)

XX_test.gogo test -bench .

一,逐步改進的方法

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(是如何存儲字符串)。

所以代替:

var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

我們可以用:

var letters = []bytes("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

甚至更好的:

const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
conststring常量len(letters)constslen(s)
string

所以我們的下一個方法如下:

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.Int31n()
rand.Int63()
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()521<<63 - 1
0..50..12..50..16/322..55/32

4.掩碼

52 = 110100brand.Int63()0..len(letterBytes)-1
len(letterBytes)0.50.25n次pow(0.5, n)(64-52)/64 = 0.191e-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 = 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/randRead(b []byte)crypto/rand
math/randrand.Randrand.Sourcerand.SourceInt63() int64
rand.Randrandrand.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)
}
math/randRandrand.Source
math/rand

The default Source is safe for concurrent use by multiple goroutines.

rand.NewSource()Sourcerand.NewSource()Source可以

二、評測

好吧,讓我們對不同的解決方案進行基準測試。

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

掩碼(並且在大索引的情況下重複)減慢一點(由於重複調用):-20%……

rand.Int63()
rand.Sourcerand.Rand
RandStringBytesMaskImprSrc()RandStringRunes()

參考資料