這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。我經常有一些困惑,`crypto/rand` 包和 `math/rand` 包是如何關聯的,或者它們是如何按照預期的方式(一起)工作的?這是其他人已經思考過的問題,還是僅僅我個人的突發奇想呢?終於有一天,我決定攻克這個問題,這篇部落格就是這次努力的結果。## `math` 包如果你曾經關注過 `math/rand` 包,你會同意它提供了相當易用的 API。我最喜歡的例子是 `func Intn(n int) int` 函數,它返回了一個你指定範圍內的隨機數。非常有用!你也許會問,頂級函數和 `Rand` 類型的執行個體函數之間有什麼異同。 如果你看了[原始碼實現](https://golang.org/src/math/rand/rand.go)的話,你會發現,頂級函數只是一個易用性的封裝,內部指向了一個包全域對象 `globalRand`。儘管這個包有些使用上的小陷阱。最基礎的用法是提供一個**偽隨機**數作為種子。這就意味著,如果你使用相同的種子來產生兩個 `Rand` 執行個體,對這兩個執行個體進行相同次序和函數的調用,那麼將會得到兩串 *完全相同* 的輸出。(我發現這顛覆了我對“隨機數”這個概念的認知,因為我可不希望能夠預測到“隨機”的結果。)如果兩個 `Rand` 對象使用了不同的值來做種子,就不具有這種相同的行為了。## `crypto` 包現在,我們來看一下 `crypto/rand` 包。這是一個精密和精確的 API 介面。我的理解是,它基於作業系統底層的隨機數產生器,產生完全不同的隨機序列。唯一的問題是:我要如何使用它???我能夠得到一個隨機的 0 和 1 的位元組切片,但是怎麼處理呢?這個不像 `math/rand` 包那麼便於使用,不是嗎?嗯,是否可以既得到 `crypto/rand` 包的真隨機性,又獲得 `math/rand` 包的易用性呢?或許真正的問題是:如何將這兩個截然不同的包組合在一起?## 一加一大於二(注意: 參考視頻 [VINTAGE 80'S REESES PEANUT BUTTER CUPS COMMERCIAL W WALKERS](https://www.youtube.com/watch?v=DJLDF6qZUX0))讓我們深入研究下 `math/rand` 包。我們通過一個 `rand.Source` 來執行個體化 `rand.Rand` 類型。但是像絕大多數 Go 慣用法一樣,這個 `Source` 是一個介面。我的第六感來了,或許這就是個機會?`rand.Source` 最主要的工作由 `Int63() int64` 函數完成,它返回一個非負 `int64` 整數(也就是說,最高位是0)。進一步改進的 `rand.Source64` 僅僅返回一個 `uint64` 類型,並沒有對最高位有任何限制。你們說,我們使用源自 `crypto/rand` 包的功能來嘗試建立一個 `rand.Source64` 對象如何?(你可以參考在 [Go Playground](https://play.golang.org/p/_3w6vWTwwE) 上的代碼。)首先,我們為我們的 `rand.Source64` 建立一個結構。(同時需要注意:因為 `math/rand` 和 `crypto/rand` 使用的時候會發生衝突,在下面的代碼中,我們將依次使用 `mrand` 和 `cand` 來代替。)```gotype mySrc struct{}```讓我們來為介面聲明 `Seed(...)` 函數。我們不需要一個和 `crypto/rand` 包互動的種子,所以沒有具體代碼。```gofunc (s *mySrc) Seed(seed int64) { /*no-op*/ }```因為 `Uint64()` 函數傳回值取值範圍**最廣(widest)**,需要 64 位元的隨機數,因此我們首先實現它。我們使用 `encoding/binary` 包從`crypto/rand` 包的 `io.Reader` 介面中讀取 8 個位元組的資料,並直接轉換成 `uint64`。```gofunc (s *mySrc) Uint64() (value uint64) {binary.Read(crand.Reader, binary.BigEndian, &value)return value}````Int63()` 函數和 `Uint64()` 函數類似,我們只要保證最高位為 0 即可。這個相當簡單,只需要在 `Uint64()` 傳回值的基礎上做一個快速的位元遮罩操作即可。```gofunc (s *mySrc) Int63() int64 {return int64(s.Uint64() & ^uint64(1<<63))}```非常棒!現在我們有了完整版的 `rand.Source64` 實現了。讓我們進行一些測試來驗證。```govar src mrand.Source64src = &mySrc{}r := mrand.New(src)fmt.Printf("%d\n", r.Intn(23))```## 權衡酷,通過上面短短十幾行代碼,我們有了一個非常簡單的解決方案,將密碼安全的隨機數產生和 `math/rand` 包友好方便的 API 有機的結合在一起。然而,我逐漸認識到沒有什麼事情是免費的。使用這個方案的代價是什嗎?讓我們來對這段代碼進行[效能分析](https://dave.cheney.net/2013/06/30/how-to-write-benchmarks-in-go)。(注意: 我喜歡在測試中使用質數,所以你會看到許多 7919 作為參數,它是第 1000 個質數。)`math/rand` 包中頂級函數的效能到底如何?```gofunc BenchmarkGlobal(b *testing.B) {for n := 0; n < b.N; n++ {result = rand.Intn(7919)}}```還不錯!在我的筆記本上大約 38 ns/op。```BenchmarkGlobal-4 50000000 37.7 ns/op```如果建立一個以目前時間作為種子的 `rand.Rand` 執行個體,情況會如何呢?```gofunc BenchmarkNative(b *testing.B) {random := rand.New(rand.NewSource(time.Now().UnixNano()))for n := 0; n < b.N; n++ {result = random.Intn(7919)}}```大約 23 ns/op,相當不錯!```BenchmarkNative-4 100000000 22.7 ns/op```現在,讓我們測試一下我們寫的新種子方案。```gofunc BenchmarkCrypto(b *testing.B) {random := rand.New(&mySrc{})for n := 0; n < b.N; n++ {result = random.Intn(7919)}}```哎呀,大約 900 ns/op,這個代價太昂貴了。是不是什麼地方我們搞錯了?或者這就是使用 `crypto/rand` 包需要付出的代價?```BenchmarkCrypto-4 2000000 867 ns/op```讓我們測試一下單獨讀取 `crypto/rand` 需要多長時間。```gofunc BenchmarkCryptoRead(b *testing.B) {buffer := make([]byte, 8)for n := 0; n < b.N; n++ {result, _ = crand.Read(buffer)}}```好,結果顯示,我們新的解決方案中絕大部分時間花在了與 `crypto/rand` 包的互動上面。```BenchmarkCryptoRead-4 2000000 735 ns/op```我不知道如何做才能進一步提高效能。而且,或許對於你的使用情境來說,花費大約1毫秒來擷取非特定隨機數不是一個問題。這個需要你自己去評估了。## 另外一種思路?我最熟悉的隨機化的用法之一是[指數退避](https://en.wikipedia.org/wiki/Exponential_backoff)工具。這樣做的目的是在重新串連到有壓力的伺服器時減少偶然同步的幾率,因為有規律的負荷可能會對伺服器的恢複造成傷害。在這些情境中,“確定性隨機”行為本身不是一個問題,但是在一群執行個體中使用相同的種子會存在問題。並且,使用頂級 `math/rand` 函數的時候,無論是使用預設的種子(即以隱含的 1 為種子),還是使用非常容易觀察的 `time.Now().UnitNano()` 範式來做種子,這都會是一個問題。如果你的服務碰巧在同一時間啟動,會在確定隨機輸出導致意外同步的情況下,服務被迫中止退出。如果我們在執行個體化的時候使用 `crypto/rand` 的強大能力來產生 `math/rand` 工具的種子,在之後,我們依然可以享受到確定性隨機工具帶來的效能,這個主意怎麼樣?```gofunc NewCryptoSeededSource() mrand.Source {var seed int64binary.Read(crand.Reader, binary.BigEndian, &seed)return mrand.NewSource(seed)}```我們可以重新對新代碼做效能分析,但是我們早已經知道,效能將回到確定性隨機的情況下。```gofunc BenchmarkSead(b *testing.B) {random := mrand.New(NewCryptoSeededSource())for n := 0; n < b.N; n++ {result = random.Intn(7919)}}```現在,我們證實了我們的假設是正確的。```BenchmarkSeed-4 50000000 23.9 ns/op```## 關於作者嗨,我是內爾·卡彭鐵爾。我是舊金山 [Orion Lab](https://www.orionlabs.io/) 的資深軟體工程師。我已經寫了三年的 Go 代碼,當快速熟悉了之後,Go 已經成為我最喜歡的語言之一了。免責聲明:我既不是安全專家,也不是跨平台 `crypto/rand` 實現專家。如果你要在關鍵安全任務用例中使用這些工具,你可以諮詢當地的安全專家。你可以從[這裡](https://github.com/orion-labs/go-crypto-source)擷取一份精鍊版的程式碼範例。它遵循 Apache 2.0 授權,所以你可以隨意剪下和借鑒任何你需要的代碼!
via: https://blog.gopheracademy.com/advent-2017/a-tale-of-two-rands/
作者:Nelz Carpentier 譯者:arthurlee 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
746 次點擊 ∙ 1 贊