¿Cómo generar una cadena aleatoria de una longitud fija en Go?

Solo quiero una cadena aleatoria de caracteres (mayúsculas o minúsculas), sin números, en Ir. ¿Cuál es la forma más rápida y sencilla de hacer esto?

La solución de Paul proporciona una solución simple y general.

La pregunta pregunta por “la forma más rápida y sencilla” . Vamos a abordar esto. Llegaremos a nuestro último código más rápido de forma iterativa. Se puede encontrar una evaluación comparativa de cada iteración al final de la respuesta.

Todas las soluciones y el código de evaluación comparativa se pueden encontrar en el Go Playground . El código en el patio de recreo es un archivo de prueba, no un ejecutable. Tienes que guardarlo en un archivo llamado XX_test.go y ejecutarlo con go test -bench . .

I. Mejoras

1. Génesis (Runas)

Como recordatorio, la solución general original que estamos mejorando es esta:

 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. Bytes

Si los caracteres para elegir y ensamblar la cadena aleatoria solo contienen las letras mayúsculas y minúsculas del alfabeto inglés, podemos trabajar con bytes solo porque las letras del alfabeto inglés se asignan a los bytes 1 a 1 en la encoding UTF-8 (que es cómo Go almacena cadenas).

Entonces, en lugar de:

 var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 

nosotros podemos usar:

 var letters = []bytes("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 

O mejor:

 const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 

Ahora, esto ya es una gran mejora: podríamos lograr que sea un const (hay constantes de string pero no hay constantes de corte ). Como ganancia adicional, la expresión len(letters) también será una const ! (La expresión len(s) es constante si s es una constante de cadena).

¿Y a qué costo? Nada en absoluto. string s puede indexarse ​​que indexa sus bytes, perfecto, exactamente lo que queremos.

Nuestro próximo destino se ve así:

 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. Resto

Las soluciones anteriores obtienen un número aleatorio para designar una letra aleatoria llamando a rand.Intn() que delega a Rand.Intn() que delega a Rand.Int31n() .

Esto es mucho más lento en comparación con rand.Int63() que produce un número aleatorio con 63 bits aleatorios.

Entonces podríamos simplemente llamar a rand.Int63() y usar el rest después de dividir por 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) } 

Esto funciona y es significativamente más rápido, la desventaja es que la probabilidad de todas las letras no será exactamente la misma (suponiendo que rand.Int63() produzca todos los números de 63 bits con la misma probabilidad). Aunque la distorsión es extremadamente pequeña ya que el número de letras 52 es mucho más pequeño que 1<<63 - 1 , en la práctica esto está perfectamente bien.

Para que esto sea más fácil: digamos que quieres un número aleatorio en el rango de 0..5 . Usando 3 bits aleatorios, esto produciría los números 0..1 con doble probabilidad que desde el rango 2..5 . Usando 5 bits aleatorios, los números en el rango 0..1 ocurrirían con 6/32 probabilidad y los números en el rango 2..5 con 5/32 probabilidad, que ahora está más cerca del deseado. Aumentar el número de bits hace que esto sea menos significativo, cuando se alcanzan los 63 bits, es insignificante.

4. Enmascaramiento

Sobre la base de la solución anterior, podemos mantener la distribución equitativa de las letras utilizando solo la mayor cantidad de los bits más bajos del número aleatorio, ya que se requieren muchos para representar el número de letras. Entonces, por ejemplo, si tenemos 52 letras, se requieren 6 bits para representarla: 52 = 110100b . Por lo tanto, solo utilizaremos los 6 bits más bajos del número devuelto por rand.Int63() . Y para mantener una distribución igual de letras, solo "aceptamos" el número si está dentro del rango 0..len(letterBytes)-1 . Si los bits más bajos son mayores, lo descartamos y consultamos un nuevo número aleatorio.

Tenga en cuenta que la probabilidad de que los bits más bajos sean mayores que o iguales a len(letterBytes) es menor que 0.5 en general ( 0.25 en promedio), lo que significa que incluso si este fuera el caso, repetir este caso "raro" disminuye la posibilidad de no encontrar un buen número. Después de la repetición n , la probabilidad de que no tengamos un buen índice es mucho menor que pow(0.5, n) , y esto es solo una estimación superior. En el caso de 52 letras, la posibilidad de que los 6 bits más bajos no sean buenos es solamente (64-52)/64 = 0.19 ; lo que significa, por ejemplo, que las posibilidades de no tener un buen número después de 10 repeticiones es 1e-8 .

Así que aquí está la solución:

 const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" const ( letterIdxBits = 6 // 6 bits to represent a letter index letterIdxMask = 1< 

5. Enmascaramiento mejorado

La solución anterior solo usa los 6 bits más bajos de los 63 bits aleatorios devueltos por rand.Int63() . Esto es un desperdicio ya que obtener los bits aleatorios es la parte más lenta de nuestro algoritmo.

Si tenemos 52 letras, eso significa 6 bits codifica un índice de letras. Entonces, 63 bits aleatorios pueden designar 63/6 = 10 índices de letras diferentes. Usemos todos esos 10:

 const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" const ( letterIdxBits = 6 // 6 bits to represent a letter index letterIdxMask = 1<= 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. Fuente

El enmascaramiento mejorado es bastante bueno, no hay mucho que podamos mejorar. Podríamos, pero no vale la pena la complejidad.

Ahora busquemos algo más para mejorar. La fuente de números aleatorios.

Hay un paquete crypto/rand que proporciona una función de Read(b []byte) , así que podríamos usar eso para obtener tantos bytes con una sola llamada como necesitemos. Esto no ayudaría en términos de rendimiento ya que crypto/rand implementa un generador de números pseudoaleatorios criptográficamente seguro, por lo que es mucho más lento.

Así que math/rand paquete de math/rand . El rand.Rand usa un rand.Source como fuente de bits aleatorios. rand.Source es una interfaz que especifica un Int63() int64 : exactamente y lo único que necesitamos y utilizamos en nuestra última solución.

Así que realmente no necesitamos un rand.Rand (ya sea explícito o global, compartido del paquete rand ), un rand.Source es perfectamente suficiente para nosotros:

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

También tenga en cuenta que esta última solución no requiere que inicialice (inicialice) el Rand global del paquete math/rand ya que no se usa (y nuestro rand.Source está inicializado / sembrado correctamente).

Una cosa más a tener en cuenta aquí: documento del paquete de estados math/rand :

La fuente predeterminada es segura para uso concurrente por múltiples goroutines.

Por lo tanto, la fuente predeterminada es más lenta que una Source que puede ser obtenida por rand.NewSource() , porque la fuente predeterminada debe proporcionar seguridad bajo acceso / uso concurrente, mientras que rand.NewSource() no ofrece esto (y por lo tanto, la Source devuelta por eso es más probable que sea más rápido).

(7. Usando rand.Read() )

Go 1.7 agregó una función rand.Read() y un método Rand.Read() . Deberíamos tener la tentación de usarlos para leer tantos bytes como necesitemos en un solo paso, para lograr un mejor rendimiento.

Hay un pequeño "problema" con esto: ¿cuántos bytes necesitamos? Podríamos decir: tantas como la cantidad de letras de salida. Pensamos que esta es una estimación superior, ya que un índice de letras utiliza menos de 8 bits (1 byte). Pero en este punto ya estamos empeorando (ya que obtener los bits aleatorios es la "parte difícil"), y estamos obteniendo más de lo necesario.

También tenga en cuenta que para mantener la distribución equitativa de todos los índices de letras, puede haber algunos datos aleatorios "basura" que no podremos usar, por lo que terminaríamos omitiendo algunos datos, y por lo tanto quedaremos cortos cuando pasemos por todos el segmento de bytes Tendríamos que obtener más bytes aleatorios, "recursivamente". Y ahora incluso estamos perdiendo la ventaja del "paquete de llamada única a rand " ...

Podríamos "algo" optimizar el uso de los datos aleatorios que adquirimos de math.Rand() . Podemos estimar cuántos bytes (bits) necesitaremos. 1 letra requiere bits letterIdxBits , y necesitamos n letras, por lo que necesitamos n * letterIdxBits / 8.0 bytes redondeando hacia arriba. Podemos calcular la probabilidad de que un índice aleatorio no sea utilizable (ver más arriba), por lo que podríamos solicitar más que sea "más probable" que sea suficiente (si resulta que no es así, repetimos el proceso). Podemos procesar el segmento de bytes como un "flujo de bits", por ejemplo, para lo cual tenemos una buena lib de terceros: github.com/icza/bitio (divulgación: soy el autor).

Pero el código de Benchmark aún muestra que no estamos ganando. ¿Por que es esto entonces?

La respuesta a la última pregunta se debe a que rand.Read() usa un bucle y sigue llamando a Source.Int63() hasta que llena el segmento pasado. Exactamente lo que hace la solución RandStringBytesMaskImprSrc() sin el buffer intermedio y sin la complejidad añadida. Es por eso que RandStringBytesMaskImprSrc() permanece en el trono. Sí, RandStringBytesMaskImprSrc() utiliza un rand.Source no sincronizado. rand.Source diferencia de rand.Read() . Pero el razonamiento todavía se aplica; y que se prueba si usamos Rand.Read() lugar de rand.Read() (el primero tampoco está sincronizado).

II. Punto de referencia

Muy bien, comparemos las diferentes soluciones.

 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 

Con solo pasar de las runas a los bytes, inmediatamente obtenemos un 22% de ganancia de rendimiento.

Deshacerse de rand.Intn() y usar rand.Int63() lugar da otro 24% de impulso.

El enmascaramiento (y la repetición en el caso de los índices grandes) se ralentiza un poco (debido a las llamadas repetitivas): -20% ...

Pero cuando hacemos uso de todos (o la mayoría) de los 63 bits aleatorios (10 índices de una llamada rand.Int63() ): eso acelera 3.4 veces .

Y finalmente, si nos conformamos con un rand.Source (no predeterminado, nuevo). rand.Source lugar de rand.Rand , obtenemos nuevamente un 23%.

Comparando la solución final con la inicial: RandStringBytesMaskImprSrc() es 5.6 veces más rápido que RandStringRunes() .

Puedes simplemente escribir el código para eso. Este código puede ser un poco más simple si desea confiar en que las letras son bytes individuales cuando están codificadas en UTF-8.

 package main import ( "fmt" "math/rand" ) func init() { rand.Seed(time.Now().UnixNano()) } var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") func randSeq(n int) string { b := make([]rune, n) for i := range b { b[i] = letters[rand.Intn(len(letters))] } return string(b) } func main() { fmt.Println(randSeq(10)) } 

Dos posibles opciones (puede haber más, por supuesto):

  1. Puede usar el paquete crypto/rand que admite la lectura de matrices de bytes aleatorias (de / dev / urandom) y está orientado a la generación aleatoria criptográfica. ver http://golang.org/pkg/crypto/rand/#example_Read . Sin embargo, podría ser más lenta que la generación de números pseudoaleatorios normales.

  2. Toma un número aleatorio y hash usando md5 o algo como esto.

Use el paquete uniuri , que genera cadenas uniformemente seguras (criptográficamente seguras).

Siguiendo icza's maravillosa y explicada solución de icza's , aquí hay una modificación que usa crypto/rand lugar de math/rand .

 const ( letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" // 52 possibilities letterIdxBits = 6 // 6 bits to represent 64 possibilities / indexes letterIdxMask = 1< 

Si desea una solución más genérica, que le permita pasar la porción de bytes de caracteres para crear la cadena, puede intentar usar esto:

 // SecureRandomString returns a string of the requested length, // made from the byte characters provided (only ASCII allowed). // Uses crypto/rand for security. Will panic if len(availableCharBytes) > 256. func SecureRandomString(availableCharBytes string, length int) string { // Compute bitMask availableCharLength := len(availableCharBytes) if availableCharLength == 0 || availableCharLength > 256 { panic("availableCharBytes length must be greater than 0 and less than or equal to 256") } var bitLength byte var bitMask byte for bits := availableCharLength - 1; bits != 0; { bits = bits >> 1 bitLength++ } bitMask = 1< 

Si desea pasar su propia fuente de aleatoriedad, sería trivial modificar lo anterior para aceptar un io.Reader lugar de usar crypto/rand .

Además, he encontrado un paquete que tiene muchos métodos para manipular datos falsos. Lo encontré útil para sembrar la base de datos al desarrollar https://github.com/Pallinder/go-randomdata . Podría ser útil para otra persona también

Si está dispuesto a agregar algunos caracteres a su grupo de caracteres permitidos, puede hacer que el código funcione con cualquier cosa que proporcione bytes aleatorios a través de un io.Reader. Aquí estamos usando crypto/rand .

 // len(encodeURL) == 64. This allows (x <= 265) x % 64 to have an even // distribution. const encodeURL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" // A helper function create and fill a slice of length n with characters from // a-zA-Z0-9_-. It panics if there are any problems getting random bytes. func RandAsciiBytes(n int) []byte { output := make([]byte, n) // We will take n bytes, one byte for each character of output. randomness := make([]byte, n) // read all random _, err := rand.Read(randomness) if err != nil { panic(err) } // fill output for pos := range output { // get random item random := uint8(randomness[pos]) // random % 64 randomPos := random % uint8(len(encodeURL)) // put into output output[pos] = encodeURL[randomPos] } return output } 

Aquí está mi camino) Usa math rand o crypto rand como desees.

 func randStr(len int) string { buff := make([]byte, len) rand.Read(buff) str := base64.StdEncoding.EncodeToString(buff) // Base 64 can be longer than len return str[:len] }