當前位置: 首頁>>技術問答>>正文


如何在Go中生成固定長度的隨機字符串?

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

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

所有解決方案和基準測試代碼都可以在Go Playground上找到。 Playground上的代碼是測試文件,而不是可執行文件。將其保存到名為XX_test.go的文件中,並使用go test -bench .可以運行它。

Go語言隨機字符串

一,逐步改進的方法

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"

現在這已經是一個很大的改進:我們可以將它變為const(有string常量,但是沒有slice常量)。作為額外的收益,表達式len(letters)也將是const! (如果s是一個字符串常量,則表達式len(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()相比,這要慢得多,後者產生一個63隨機bits的隨機數。

所以我們可以簡單地調用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.掩碼

在前麵的解決方案的基礎上,我們可以通過使用隨機數的最低位來保證字母的平等分配。因此,例如,如果我們有52個字母,則需要6位來表示它:52 = 110100b。所以我們隻使用rand.Int63()返回的最低6位數。並且為了保持字母的平均分配,我們隻有”接受”落在0..len(letterBytes)-1的範圍內的數字。如果最低位更大,我們將其丟棄並查詢新的隨機數。

請注意,最低位大於或等於len(letterBytes)的可能性一般小於0.5(平均為0.25),這意味著即使出現這種情況,重複此”rare”案例也會降低找不到的好的數字的可能性。在n次重複之後,我們沒有獲得好數字的機會遠小於pow(0.5, n),這隻是一個較高的估計。在52個字母的情況下,6個最低位不好的可能性僅為(64-52)/64 = 0.19;這意味著例如在10次重複之後沒有良好數字的機會是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個字母,那意味著6bits編碼字母索引。所以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實現了加密安全的偽隨機數生成器,因此速度要慢得多。

讓我們堅持使用math/rand軟件包。 rand.Rand使用rand.Source作為隨機位的來源。 rand.Source是一個指定Int63() int64方法的接口:我們最新解決方案中唯一需要和使用的方法。

所以我們真的不需要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)
}

另請注意,最後一個解決方案不要求您初始化(播種)math/rand軟件包的全局Rand,因為它未被使用(並且我們的rand.Source已正確初始化/設置種子)。

還有一點需要注意:math/rand的包文檔說明:

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

所以默認源比rand.NewSource()可能獲得的Source慢,因為默認源必須在並發訪問/使用時提供安全性,而rand.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()代替另外24%的提升。

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

但是當我們使用63個隨機位中的所有(或大多數)(來自一個rand.Int63()調用的10個索引)時:它加速了3.4倍。

最後,如果我們解決了(non-default,新)rand.Source代替rand.Rand,我們再次獲得23%。

比較最終解決方案:RandStringBytesMaskImprSrc()RandStringRunes()快5.6倍。

Go語言隨機字符串2

參考資料

 

本文由《純淨天空》出品。文章地址: https://vimsky.com/zh-tw/article/4115.html,未經允許,請勿轉載。