Generate a random string in Go

b := make([]byte, 16); rand.Read(b); token := hex.EncodeToString(b)

b := make([]byte, 16); rand.Read(b); token := hex.EncodeToString(b) fills 16 bytes from crypto/rand and writes them as 32 hex characters: 128 bits, enough for a session ID, an API key or a reset token.

package main

import (
	"crypto/rand"
	"encoding/base64"
	"encoding/hex"
	"fmt"
)

func main() {
	b := make([]byte, 16)
	if _, err := rand.Read(b); err != nil {
		panic(err)
	}
	fmt.Println(hex.EncodeToString(b))
	fmt.Println(base64.RawURLEncoding.EncodeToString(b))
}
305e47d3a2beb78023a8b088a48fccf9
MF5H06K-t4AjqLCIpI_M-Q

base64.RawURLEncoding writes the same 128 bits as 22 URL-safe characters without padding. Before Go 1.24, check the error from rand.Read. From 1.24 it never returns one: if the operating system can’t supply random bytes, the program crashes instead.

The tool below counts the hex form as 32 characters × log2(16) = 128 bits. OWASP’s Session Management Cheat Sheet sets 64 bits of entropy as the floor for a session ID; 128 is the usual target.

rand.Text, from Go 1.24

Go 1.24 added rand.Text(), which returns 26 characters of RFC 4648 base32, A to Z and 2 to 7:

fmt.Println(rand.Text())
// RCD2JSMYDDEQCADQFQIPPMBUXP

That’s 26 × 5 = 130 bits; the tool shows the same with Custom, the alphabet ABCDEFGHIJKLMNOPQRSTUVWXYZ234567 and Length 26. Its documentation promises at least 128 bits and says a future version may return longer text, so don’t count on 26. On Go 1.23 the build fails with undefined: rand.Text.

Letters and digits

rand.Int(rand.Reader, max) returns a uniform number below max, redrawing any value that would skew it, so it can index an alphabet:

const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
out := make([]byte, 22)
for i := range out {
	n, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphabet))))
	if err != nil {
		panic(err)
	}
	out[i] = alphabet[n.Int64()]
}
fmt.Println(string(out))
// jpdj637Z6eZqWqkYu9Wjda

That’s 22 characters × log2(62) = 131 bits. Don’t write alphabet[b[i]%62] over random bytes instead: 256 isn’t a multiple of 62, so A to H come up a quarter more often than the rest.

The math/rand trap

math/rand has a Read(b []byte) too, so the first program with "math/rand" in place of "crypto/rand" still compiles, passes go vet and runs. That package’s documentation says its output may be easily predictable, however it’s seeded. Before Go 1.20 it was worse: its top-level functions started from the same seed on every run. GODEBUG=randautoseed=0 still brings that back on Go 1.23 and 1.24, and every run prints the same “token”:

GODEBUG=randautoseed=0 go run main.go
# 52fdfc072182654f163f5f0f9a621d72
# Uv38ByGCZU8WP18PmmIdcg
GODEBUG=randautoseed=0 go run main.go
# 52fdfc072182654f163f5f0f9a621d72
# Uv38ByGCZU8WP18PmmIdcg

math/rand.Read is marked deprecated, and math/rand/v2, added in Go 1.22, has no top-level Read at all.

Try it in the Random String Generator

Drawing bytes from your browser’s random source…

Runs in your browser. Nothing you enter is sent anywhere.