生成随机值
概述
标准数学包提供的伪随机生成器通常速度快但可预测。在处理加密时,获得强随机生成器是至关重要的。
本页面描述如何从随机生成的位生成随机字符串和数字。
随机字符串
生成随机字符串最简单且安全的方法是生成随机字节并使用base16(十六进制)、base32或base64编码方案对其进行编码。
import (
"crypto/rand"
"encoding/base32"
)
func generateRandomString() string {
bytes := make([]byte, 12)
rand.Read(bytes)
return base32.StdEncoding.EncodeToString(bytes)
}
自定义字符集
如果字符集长度适合现有的编码方案(例如base64),可以自定义使用的字符。
import (
"crypto/rand"
"encoding/base32"
)
var customEncoding = base32.NewEncoding("0123456789ABCDEFGHJKMNPQRSTVWXYZ")
func generateRandomString() string {
bytes := make([]byte, 12)
rand.Read(bytes)
return customEncoding.EncodeToString(bytes)
}
如果不适合,则需要一个高质量的随机数生成器来生成自定义范围内的整数。
const alphabet = "abcdefg"
func generateRandomString() string {
var result string
for i := 0; i < 12; i++ {
result += string(alphabet[generateRandomInt(0, len(alphabet))])
}
return result
}
随机整数
如果范围是2的幂(2, 4, 8, 16等),可以使用简单的位掩码来实现。
bytes := make([]byte, 1)
rand.Read(bytes)
value := bytes[0] & 0x03 // 生成[0, 3]之间的随机值
对于自定义范围,一个简单的方法是生成一个非常大的随机数并使用取模操作。由于这会引入模偏差,随机整数必须足够大。例如,如果最大值是10并且我们生成32位随机数,偏差约为1/250,000,000,这对于大多数用例来说足够了。
import (
"crypto/rand"
"encoding/binary"
)
// 生成一个[0, max)之间的随机整数。
// `max`不应是非常大的数字。
func generateRandomUint32(max uint32) uint32 {
bytes := make([]byte, 4)
rand.Read(bytes)
randUint32 := binary.BigEndian.Uint32(bytes) // 将字节转换为uint32
return randUint32 % max
}
另一种常见的方法是将最大值乘以一个随机浮点数。这也可能引入偏差,但如果最大值足够小,这种偏差是分散的。
func generateRandomUint32(max uint32) uint32 {
return uint32(max * generateRandomFloat32())
}
最安全的方法是使用拒绝采样,不断生成随机值直到其小于最大值。为了增加随机值小于最大值的可能性,我们只需生成表示最大值所需的位数。例如,如果最大值是10,我们只需生成4位。在下面的代码中,我们生成一个随机字节,然后屏蔽前4位以获得4个随机位(8-4=4)。
import (
"crypto/rand"
"math/big"
)
func generateRandomUint64(max *big.Int) uint64 {
randVal := new(big.Int)
shift := max.BitLen() % 8
bytes := make([]byte, (max.BitLen() / 8) + 1)
rand.Read(bytes)
if shift != 0 {
bytes[0] &= (1 << shift) - 1
}
randVal.SetBytes(bytes)
for randVal.Cmp(max) >= 0 {
rand.Read(bytes)
if shift != 0 {
bytes[0] &= (1 << shift) - 1
}
randVal.SetBytes(bytes)
}
return randVal.Uint64()
}
生成0到1之间的随机浮点数
一种常见的方法是生成一个随机整数并将其除以一个非常大的数。在这样做时,确保分母足够大,并且分母是2的幂,以便能被float64准确表示。
func generateRandomFloat64() float64 {
return float64(generateRandomInteger(1<<53)) / (1 << 53)
}
另一种方法是为尾数(float64)生成52个随机位,并将其转换为[0, 1)之间的浮点数。这通常更快,因为它避免了除法。
import (
"crypto/rand"
"math"
)
func generateRandomFloat64() float64 {
bytes := make([]byte, 7)
rand.Read(bytes)
bytes = append(make([]byte, 1), bytes...)
// 设置指数部分为0b01111111111
bytes[0] = 0x3f
bytes[1] |= 0xf0
return math.Float64frombits(binary.BigEndian.Uint64(bytes)) - 1
}
偏差
一个非常常见的偏差是模偏差。例如,如果RANDOM_INT是[0, 10)之间的整数,某些数字会出现3次(0, 1),而其他数字会出现2次(2, 3)。
RANDOM_INT % 4
要计算近似偏差,可以使用下面的公式。
1 / ( RANDOM_BITS - LOG2(MAX) )
例如,如果我们使用8个随机位且最大值为100,近似偏差为0.6:
1 / ( 8 - LOG2(100) ) ≈ 1 / (8-6.4) ≈ 0.6
将最大值乘以随机浮点数也可能引入偏差。在此示例中,RANDOM_FLOAT在[0, 1)范围内,因此输出将在[0, 5)内。
FLOOR( RANDOM_FLOAT * 5 )
假设RANDOM_FLOAT可以是8个数之一:0, 0.125, 0.25, ..., 0.875。在这种情况下,(0, 1, 3)会出现1/4次,而(2, 4)只出现1/8次。虽然这是一个极端的例子,但确保随机浮点数相对于最大值提供足够的“随机性”是很重要的。