Skip to content

ULID Generator — Go source

Generate Universally Unique Lexicographically Sortable Identifiers (ULID) - 26-character Crockford-base32 strings that sort by millisecond timestamp. Paste any ULID to decode its timestamp and randomness. Runs entirely in your browser.

This is the Go implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Package ulid is the Go twin of CosmoDev's src/lib/ulid.ts (dual source: the
// web lib is TypeScript, the CLI lib is Go — kept in lock-step). Pure +
// deterministic, never panics. The table-driven tests in ulid_test.go share
// vectors with src/lib/ulid.test.ts so the two implementations are held to the
// same contract.
//
// The TS lib wraps the `ulid` npm package. This Go twin HAND-ROLLS the ULID
// algorithm (stdlib only — no oklog/ulid, no external module). A ULID is 26
// Crockford-base32 chars: the first 10 encode a 48-bit millisecond timestamp
// (MSB-first) and the last 16 encode 80 bits of randomness. Crockford alphabet:
// "0123456789ABCDEFGHJKMNPQRSTVWXYZ" (excludes I, L, O, U).
//
// Time-encoding is the lock-step ANCHOR. Because 32^16 == 2^80 exactly,
// packing [6 time bytes | 10 random bytes] into a 16-byte big-endian integer
// and base32-encoding the whole 128-bit value reproduces the npm package's
// separate encodeTime(ts, 10) verbatim in the first 10 chars — so
// DecodeTime(Generate(ms)) == ms holds identically across Go and TS. The
// random tail is drawn from crypto/rand (the Go analogue of the TS default
// RNG) and varies call to call, exactly as in the TS default-RNG path.
package ulid

import (
	"crypto/rand"
	"fmt"
	"strings"
	"time"
)

// crockford is the Crockford base32 alphabet (excludes I, L, O, U).
const crockford = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"

// decodeMap maps an ASCII byte to its Crockford base32 value, or 0xFF if the
// byte is not a valid ULID character. Letters I, L, O, U are invalid (absent
// from the alphabet); lowercase letters are accepted and decode identically to
// their uppercase form — matching the `ulid` npm package's DECODING table, so a
// lowercase TS-produced ULID decodes the same way in Go.
var decodeMap [256]byte

func init() {
	for i := range decodeMap {
		decodeMap[i] = 0xFF
	}
	for i := 0; i < len(crockford); i++ {
		decodeMap[crockford[i]] = byte(i)
	}
	// Accept lowercase a-z where the uppercase equivalent is a valid symbol.
	// i/l/o/u stay 0xFF because I/L/O/U are not in the alphabet.
	for c := byte('a'); c <= 'z'; c++ {
		up := c - 'a' + 'A'
		if decodeMap[up] != 0xFF {
			decodeMap[c] = decodeMap[up]
		}
	}
}

// divBy32 divides the big-endian integer stored in b by 32, in place, and
// returns the remainder (0-31). It is long division in base 256: each byte is
// combined with the carry from the previous (more-significant) byte, the high
// 8 bits become the new byte and the low 5 bits become the next carry.
func divBy32(b []byte) byte {
	var rem byte
	for i := 0; i < len(b); i++ {
		cur := uint16(rem)<<8 | uint16(b[i])
		b[i] = byte(cur >> 5)
		rem = byte(cur & 0x1F)
	}
	return rem
}

// Generate returns a new ULID for the given millisecond timestamp. It is the Go
// twin of generateUlid() in src/lib/ulid.ts.
//
// The timestamp occupies the low 48 bits of ms and is written MSB-first into
// the first 6 bytes; the last 10 bytes are filled from crypto/rand. The whole
// 16-byte value is then base32-encoded into the canonical 26 chars.
//
// Generate returns a string only (no error) for parity with the TS default-RNG
// path. If crypto/rand.Read fails — which should not happen in practice — the
// random bytes stay zero: the ULID remains well-formed and its timestamp still
// round-trips, it is merely less random. Never panics. Callers should pass a
// non-negative ms-since-epoch; negative values encode the low 48 bits of the
// two's-complement representation (defined but not meaningful as a timestamp).
func Generate(ms int64) string {
	var b [16]byte
	u := uint64(ms)
	b[0] = byte(u >> 40)
	b[1] = byte(u >> 32)
	b[2] = byte(u >> 24)
	b[3] = byte(u >> 16)
	b[4] = byte(u >> 8)
	b[5] = byte(u)
	_, _ = rand.Read(b[6:16]) // best-effort; on failure b[6:16] stays zero

	// Repeatedly divide the 128-bit value by 32, collecting remainders LSB-first
	// into out[25] down to out[0]. 26 base32 digits cover 130 bits, so the most
	// significant digit captures the leftover top 3 bits (value 0-7).
	var out [26]byte
	for i := 25; i >= 0; i-- {
		out[i] = crockford[divBy32(b[:])]
	}
	return string(out[:])
}

// DecodeTime extracts the 48-bit millisecond timestamp encoded in the first 10
// chars of id. It is the Go twin of decodeUlidTime() in src/lib/ulid.ts.
//
// It returns an error if id is not exactly 26 chars long or contains a
// character outside the Crockford base32 alphabet (I, L, O, U are invalid), so
// it errors on the same inputs as the `ulid` npm package's decodeTime.
func DecodeTime(id string) (int64, error) {
	if len(id) != 26 {
		return 0, fmt.Errorf("ulid: malformed id: length %d, want 26", len(id))
	}
	var ts int64
	for i := 0; i < 10; i++ {
		v := decodeMap[id[i]]
		if v == 0xFF {
			return 0, fmt.Errorf("ulid: invalid character %q at position %d", rune(id[i]), i)
		}
		ts = ts*32 + int64(v)
	}
	return ts, nil
}

// Info is the Go twin of the UlidInfo interface in src/lib/ulid.ts: the
// structured breakdown of a pasted ULID. Error is set only when Valid is false.
type Info struct {
	Valid      bool
	Error      string
	TimePart   string // first 10 chars — the encoded timestamp
	Randomness string // last 16 chars — the random part
	Timestamp  int64  // decoded ms since epoch
	ISO        string // timestamp as ISO-8601 UTC with millisecond precision
}

// timeMax is the 48-bit ULID timestamp ceiling (281474976710655 ms, year 10889).
const timeMax = int64(1)<<48 - 1

// Inspect validates a pasted ULID and breaks out its structural parts. It is
// the Go twin of inspectUlid() in src/lib/ulid.ts, sharing its test vectors.
// Input is trimmed and uppercased before validation (matching the TS twin);
// error strings mirror the TS messages so both implementations are held to
// one contract. Never panics.
func Inspect(input string) Info {
	id := strings.ToUpper(strings.TrimSpace(input))
	if len(id) != 26 {
		return Info{Error: fmt.Sprintf("Length must be 26 characters, got %d", len(id))}
	}
	for i := 0; i < len(id); i++ {
		if decodeMap[id[i]] == 0xFF {
			return Info{Error: "Invalid character — ULIDs use Crockford base32 (no I, L, O, U)"}
		}
	}
	ts, err := DecodeTime(id)
	if err != nil || ts > timeMax {
		return Info{Error: "Time part overflows the 48-bit ULID timestamp ceiling"}
	}
	return Info{
		Valid:      true,
		TimePart:   id[:10],
		Randomness: id[10:],
		Timestamp:  ts,
		ISO:        time.UnixMilli(ts).UTC().Format("2006-01-02T15:04:05.000Z"),
	}
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →