go / crockford base32
I use Crockford's Base32 when I want a short identifier that a human will type or read aloud: email confirmation codes, invite tokens, short URL slugs.
Alphabet
0123456789ABCDEFGHJKMNPQRSTVWXYZ
It drops I, L, O, and U to remove visual ambiguity
and avoid accidental obscenities.
On decode, any case is accepted,
I and L map to 1, and O maps to 0,
so users can type what they think they see.
For contrast, esbuild emits standard RFC 4648 Base32
(uppercase A–Z plus 2–7) for build artifacts
like app-2H67SL6V.css (see cmd/esbuild).
That alphabet is fine for filenames a CDN serves;
it's not meant for humans.
Encoding
Go's encoding/base32 accepts a custom alphabet:
package crockford
import (
"encoding/base32"
"strings"
)
const alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
var enc = base32.NewEncoding(alphabet).WithPadding(base32.NoPadding)
// Encode returns the Crockford Base32 encoding of src.
func Encode(src []byte) string {
return enc.EncodeToString(src)
}
// normalize maps confusables to their canonical forms
// and strips formatting hyphens.
var normalize = strings.NewReplacer(
"i", "1", "I", "1",
"l", "1", "L", "1",
"o", "0", "O", "0",
"-", "",
)
// Decode parses any case, with optional hyphens
// and the I/L→1, O→0 substitutions Crockford allows.
func Decode(s string) ([]byte, error) {
return enc.DecodeString(strings.ToUpper(normalize.Replace(s)))
}
Confirmation codes
For an email confirmation flow, generate 5 random bytes, encode to 8 characters, and group with a hyphen for readability:
import "crypto/rand"
// NewCode returns an 8-character Crockford Base32 code
// formatted as XXXX-XXXX.
func NewCode() string {
var b [5]byte
if _, err := rand.Read(b[:]); err != nil {
panic(err)
}
s := Encode(b[:])
return s[:4] + "-" + s[4:]
}
NewCode() // "2H67-SK6V"
NewCode() // "JBTS-FCY2"
40 bits gives ~1 trillion possibilities, plenty for a one-time code with a short TTL and a small wrong-guess limit.
Verify with constant-time comparison:
import "crypto/subtle"
func Verify(input, want string) bool {
got, err := Decode(input)
if err != nil {
return false
}
exp, _ := Decode(want)
return subtle.ConstantTimeCompare(got, exp) == 1
}
Decode tolerates the user typing o for 0, lowercase letters,
and stray hyphens, so 2h67-sk6v, 2H67SK6V,
and 2H67-SK6V all verify the same.
When to use
- One-time codes a human types from another device
- Short share or invite tokens in URLs
- Short IDs surfaced in CLI output
When not to use
- Opaque IDs that machines never read aloud (use ULID, UUID, or hex)
- Build-time content hashes (esbuild's standard Base32 is fine)
- Inputs that must round-trip exactly — the decoder normalizes