skip to content

What does crypto/rand.Text() return in Go, and why prefer it over hand-rolling a token?

level: middleimportance: should knowfreq 40%

answer

  1. one call, no arguments
  2. returns a string, not bytes
  3. the alphabet has 32 symbols
  4. twenty-six characters, at least 128 bits
  5. nothing to check because it cannot fail

basics

~10 s

crypto/rand.Text returns a 26-character string over the base32 alphabet A-Z and 2-7, carrying at least 128 bits of randomness. It takes no arguments and returns no error, so a token is one call.

solid answer

~50 s

`crypto/rand.Text()` was added in Go 1.24 for exactly this job: a short secret string such as a password-reset token, an invite code or an API key. It reads from the same operating-system source as `crypto/rand.Read`, maps each byte onto a 32-symbol base32 alphabet, and returns 26 characters — the documented guarantee is at least 128 bits of randomness. The signature is `func Text() string`: no length parameter, no error to check. That is the point. Hand-rolled equivalents are where the bugs live: a slice sized by guesswork, an alphabet mapping that skews the distribution, an ignored error, or a fallback generator in the error branch. The output is alphanumeric and uppercase, so it survives a URL, an email body and being read aloud down a phone. For raw key bytes use `crypto/rand.Read`; for a bounded number use `crypto/rand.Int`.

code

go · 7 lines
go
import "crypto/rand"

// Text takes no arguments and returns no error.
func resetLink(base string) (token, link string) {
	token = rand.Text() // 26 chars, A-Z and 2-7
	return token, base + "?t=" + token
}

go deeper

for a junior

Remember that Go has a single call that hands you a ready-to-use secret string, that it lives in crypto/rand, and that it needs no arguments and no error check.

for a middle

Describe the shape of the output — 26 characters over a 32-symbol base32 alphabet — and explain why an API with no length and no alphabet parameter removes the mistakes that hand-rolled token helpers keep making.

for a senior

Show the judgment about when it does not fit: raw key material, a numeric code, or a format the product dictates. Be able to name crypto/rand.Read and crypto/rand.Int as the right calls for those and say why you would not slice Text's output.

for a principal

Own the convention. Decide that token generation is one helper in one package, so the format is consistent across services and a future change of shape is a single edit rather than a hunt through every place that built a string out of random bytes.

## The call ```go func Text() string ``` `crypto/rand.Text` takes nothing and returns a string. It fills 26 bytes from the package's cryptographic source and maps each one onto the standard RFC 4648 base32 alphabet — the 26 uppercase letters plus the digits 2 through 7, 32 symbols in total. Twenty-six symbols of five bits each is why the documentation can promise at least 128 bits of randomness. Because the alphabet has exactly 32 symbols and a byte has 256 values, the mapping divides evenly: every symbol is equally likely. That is not an accident of the implementation, it is why base32 was chosen over, say, a 62-character alphanumeric alphabet, where reducing a random byte onto the alphabet would skew the result unless you handle it carefully. ## Why the API has no knobs Every parameter a token API exposes is a parameter somebody gets wrong. `Text` has none: - **No length.** You cannot ask for a shorter token and quietly drop below the guarantee the documentation makes. If you slice the result to ten characters you have thrown that guarantee away, and you now own the arithmetic. - **No alphabet.** You cannot pass a character set whose size makes the reduction skew. - **No error.** It calls `crypto/rand.Read`, which since Go 1.24 always fills its buffer and never returns an error. So there is no error branch — and therefore no place for the classic bug of falling back to a statistical generator when the "impossible" error fires. What you lose is control over format. The output is uppercase and alphanumeric; if your product needs lowercase, hyphen groups or a numeric code, `Text` is not it and you build from `crypto/rand.Read` or `crypto/rand.Int` instead. ## What it replaces Before Go 1.24, the standard shape was: allocate a byte slice, fill it with `crypto/rand.Read`, then turn the bytes into a printable string with an encoder. That code is fine when written carefully, and it is still the right thing when you need a specific format. But it is four decisions wide — slice length, encoder, error handling, and whether the encoder's output is URL-safe — and a review has to check all four every time. `rand.Text()` is one decision wide, and the decision has already been made by the standard library. ## Where it fits in a reset-link worker A worker that consumes a queue of reset requests and hands links to a mailer wants one value per message, generated locally, with no failure mode: ```go func resetLink(base string) (token, link string) { token = rand.Text() return token, base + "?t=" + token } ``` No error path, nothing to plumb, nothing to mock. The characters need no percent-encoding in a query string, and they are unambiguous when a user retypes them, which is why base32 excludes the digits 0 and 1 and the letters they resemble. ## Neighbouring calls in the same package - `crypto/rand.Read(b []byte)` — fills a slice with random bytes. This is what you want for a key, a salt, or anything you will pass to a cipher rather than show to a person. - `crypto/rand.Int(rand io.Reader, max *big.Int) (*big.Int, error)` — returns a uniformly distributed value in the half-open range `[0, max)`. This is the call for a six-digit confirmation code or for choosing an index into a custom alphabet. It takes and returns `*big.Int`, which is clumsy for small numbers but is what keeps the distribution uniform; it panics if `max` is not positive. - `crypto/rand.Reader` — the `io.Reader` behind all of them, and the value you pass as the first argument to `Int`. ## The reviewer's rule of thumb If the value is a short secret a human or a URL will carry, reach for `Text`. If it is bytes a cryptographic primitive will consume, reach for `Read`. If it is a number in a range, reach for `Int`. A hand-rolled string built out of `Read` plus a custom alphabet needs a reason in the pull-request description, because the standard library now covers the common case and the custom version has more places to be wrong.

  • The product wants a six-digit numeric confirmation code instead. What does the standard library give you?
    `crypto/rand.Int(rand.Reader, big.NewInt(1000000))` returns a uniformly distributed `*big.Int` in `[0, 1000000)`, which you format with `%06d`. It reports an error and panics if the bound is not positive. Do not take a random byte and reduce it with a remainder onto your range by hand — `Int` exists so you do not have to reason about the distribution yourself.
  • Can you shorten crypto/rand.Text output to ten characters to make the link tidier?
    You can slice the string, but you have then given up the guarantee the documentation attaches to the full result and taken ownership of the arithmetic yourself. The API deliberately offers one size so that nobody trims it casually. If the link length is a real product constraint, treat shortening as a decision to argue for explicitly, not a formatting tweak.
  • Is the output of crypto/rand.Text safe to put straight into a URL?
    Yes. The base32 alphabet is uppercase letters and the digits 2 through 7, all of which are unreserved in a URL, so nothing needs percent-encoding. It is case-sensitive as far as your lookup is concerned, so store and compare it exactly as generated rather than upper- or lower-casing it on one side only.

saying these in an interview costs you the question

  • Thinks Text takes a length argument
  • Expects a byte slice rather than a string
  • Trims the result and keeps claiming the same strength
  • Rebuilds the same thing from Read plus a custom alphabet
  • Reaches for Text when raw key bytes are needed