A Go library that generates concise, unambiguous, URL-safe UUIDs. Based on and
compatible with the Python library
shortuuid.
Often, one needs to use non-sequential IDs in places where users will see them,
but the IDs must be as concise and easy to use as possible. shortuuid solves
this problem by generating UUIDs using the standard library
uuid package (Go 1.27+) and then translating them to
base57 using lowercase and uppercase letters and digits, and removing
similar-looking characters such as l, 1, I, O and 0.
package main
import (
"fmt"
"github.com/lithammer/shortuuid/v5"
)
func main() {
u := shortuuid.New()
fmt.Println(u) // KwSysDpxcBU9FNhGkn2dCf
}New returns a v4 (random) UUID. Name a version explicitly when you want a
different one:
shortuuid.NewV4() // random, same as New
shortuuid.NewV7() // time-orderedv7 UUIDs lead with a millisecond timestamp, and base57 keeps that order, so sorting v7 shortuuids as strings sorts them by creation time.
That timestamp is readable once decoded. The standard library uuid package
has no accessor for it, so read the bits directly — the first 48 hold the Unix
time in milliseconds:
// timeV7 returns the time a UUIDv7 was created, as defined in RFC 9562,
// Section 5.7. The bool reports whether u is a v7 UUID at all.
func timeV7(u uuid.UUID) (time.Time, bool) {
if u[6]>>4 != 7 {
return time.Time{}, false
}
ms := binary.BigEndian.Uint64(u[:8]) >> 16
return time.UnixMilli(int64(ms)), true
}
u, _ := shortuuid.DefaultEncoder.Decode("2JWDejXMnCMQungUr4fCjm")
t, ok := timeV7(u) // t is 2026-09-02T03:05:45.235Z, ok is trueFor v5 (derived from a name rather than random) pass a namespace and a name:
shortuuid.NewV5(shortuuid.NameSpaceDNS, "example.com") // exu3DTbj2ncsn9tLdLWspw
shortuuid.NewV5(shortuuid.NameSpaceURL, "http://example.com") // T35fvrnVz6SMSdh9y5hs8c
shortuuid.NewV5(shortuuid.NameSpaceOID, "1.2.840.113549") // HVizdopCKiLaGoTrVJrg9r
shortuuid.NewV5(shortuuid.NameSpaceX500, "CN=example,O=org")Pass the bare name — an OID is "1.2.840.113549", not "urn:oid:1.2.840.113549".
Decoding turns a shortuuid back into a UUID:
u, err := shortuuid.DefaultEncoder.Decode("KwSysDpxcBU9FNhGkn2dCf")
// 64d1355f-d052-4bd9-83f4-39b93fb1c01fDecoding is not a validity check. It rejects characters outside the alphabet and values that need more than 128 bits, but pads input that is too short, so an ID that lost a character decodes to a different UUID rather than failing. Compare the length as well:
func valid(s string) bool {
_, err := shortuuid.DefaultEncoder.Decode(s)
return err == nil && len(s) == 22
}A custom alphabet encodes to its own width, and its characters may take more than one byte each, so count runes and ask the encoder for the width:
var (
enc = shortuuid.NewEncoder(abc)
width = utf8.RuneCountInString(enc.Encode(uuid.UUID{}))
)
func valid(s string) bool {
_, err := enc.Decode(s)
return err == nil && utf8.RuneCountInString(s) == width
}A custom alphabet (at least 2 distinct characters) needs its own encoder. Sorting and deduplicating the alphabet happens up front, so build the encoder once and reuse it:
var enc = shortuuid.NewEncoder("23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxy=")
enc.Encode(uuid.New())The encoder takes any UUID, so one encoder covers every version:
enc.Encode(uuid.NewV7())
enc.Encode(shortuuid.UUIDv5(shortuuid.NameSpaceDNS, "example.com")) // dwt3CSai2mbrm9sKcKVrovDecode with the same encoder, since the alphabet has to match:
u, err := enc.Decode("dwt3CSai2mbrm9sKcKVrov")
// cfbff0d1-9375-5685-968c-48ce8b15ae17Bring your own encoder! For example, base58 is popular among cryptocurrencies like Bitcoin.
package main
import (
"fmt"
"uuid"
"github.com/btcsuite/btcutil/base58"
"github.com/lithammer/shortuuid/v5"
)
type base58Encoder struct{}
func (enc base58Encoder) Encode(u uuid.UUID) string {
return base58.Encode(u[:])
}
func (enc base58Encoder) Decode(s string) (uuid.UUID, error) {
b := base58.Decode(s)
if len(b) != 16 {
return uuid.UUID{}, fmt.Errorf("invalid UUID (got %d bytes)", len(b))
}
return uuid.UUID(b), nil
}
func main() {
enc := base58Encoder{}
fmt.Println(shortuuid.NewWithEncoder(enc)) // 6R7VqaQHbzC1xwA5UueGe6
}Migrating from v4
New is unchanged, so code calling it only needs its import path bumped to
/v5.
Encoder now names the standard library uuid.UUID rather than
github.com/google/uuid.UUID, so a custom encoder stops satisfying the
interface until it imports uuid instead. Both types are [16]byte, so a
google/uuid value converts with uuid.UUID(v).
NewWithEncoder and NewWithAlphabet still work, but are deprecated. Use
NewEncoder instead. NewWithEncoder(enc) is enc.Encode(uuid.New()) at the
same cost, and encoding directly also reaches the other UUID versions.
NewWithAlphabet re-sorts the alphabet on every call with no way to avoid it,
so a reused encoder is more than twice as fast:
shortuuid.NewWithAlphabet(abc) // deprecated
var enc = shortuuid.NewEncoder(abc) // hoisted once
enc.Encode(uuid.New())NewWithNamespace(name) still works and its IDs are unchanged, but it is
deprecated. It guesses the namespace from the name: NameSpaceURL for
http:// and https:// prefixes, matched case-insensitively, and
NameSpaceDNS for everything else, which is what the Python shortuuid
library does. An OID or an X.500 DN hashes under NameSpaceDNS too.
Prefer NewV5 with the namespace you mean:
shortuuid.NewWithNamespace("http://example.com") // deprecated
shortuuid.NewV5(shortuuid.NameSpaceURL, "http://example.com") // same ID, explicitReproducing the guess takes a small helper:
func fromName(name string) string {
if name == "" {
return shortuuid.New()
}
lower := strings.ToLower(name)
if strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") {
return shortuuid.NewV5(shortuuid.NameSpaceURL, name)
}
return shortuuid.NewV5(shortuuid.NameSpaceDNS, name)
}v5 requires Go 1.27 and has no dependencies.
MIT