A lightweight, stateless implementation of ULIDs (Universally Unique Lexicographically Sortable Identifiers) for Go.
Minimal overhead, zero external dependencies, and full control over entropy sources. Encode in 26-character Base32 strings that are naturally sortable and human-readable.
A ULID is a 128-bit identifier composed of:
- 48-bit timestamp (millisecond precision, sortable by creation time)
- 80-bit entropy (random component, ensures uniqueness)
This makes ULIDs perfect for database IDs, request tracing, and anywhere you need sortable unique identifiers.
01ARZ3NDEKTSV4RRFFQ69G5FAV
│ │
└─ 10-char timestamp └─ 16-char entropy (in Base32)
package main
import (
"fmt"
"lowbit.dev/ulid"
)
func main() {
id := ulid.Make()
fmt.Println(id) // e.g., 01ARZ3NDEKTSV4RRFFQ69G5FAV
fmt.Println(id.Time()) // Timestamp of generation
}id, err := ulid.Parse("01ARZ3NDEKTSV4RRFFQ69G5FAV")
if err != nil {
log.Fatal(err)
}
fmt.Println(id.Time())For deterministic testing or custom randomness:
now := time.Now()
entropy := bytes.NewReader(randomBytes)
id, err := ulid.MakeCustom(now, entropy)By default, Make() generates random ULIDs. If you need strict lexicographical ordering even for IDs generated in the same millisecond, use MonotonicGenerator:
gen := ulid.NewMonotonic(nil) // nil uses crypto/rand
id1 := gen.Make(time.Now()) // 01ARZ3NDEKTSV4RRFFQ69G5FAV
id2 := gen.Make(time.Now()) // 01ARZ3NDEKTSV4RRFFQ69G5FB0 (entropy incremented)
id3 := gen.Make(time.Now()) // 01ARZ3NDEKTSV4RRFFQ69G5FB1
// id1 < id2 < id3, even if all generated within the same millisecondThe generator maintains internal state and automatically rolls over to fresh entropy when the timestamp advances. If entropy space is exhausted within a millisecond, ErrOverflow is returned.
Make() ULID— Generate a new ULID using the current time andcrypto/rand.MakeCustom(t time.Time, entropy io.Reader) (ULID, error)— Generate a ULID with explicit time and entropy source.
Parse(s string) (ULID, error)— Parse a 26-character Base32 string into a ULID.- Supports Crockford Base32 aliases:
O→0,I/L→1 - Case-insensitive
- Supports Crockford Base32 aliases:
String() string— Format the ULID as a 26-character Base32 string.AppendText(dst []byte) []byte— Zero-allocation append to an existing byte slice.MarshalText() ([]byte, error)— JSON-compatible text marshaling.
Time() time.Time— Extract the creation timestamp from the ULID.
NewMonotonic(entropy io.Reader) *MonotonicGenerator— Create a stateful monotonic generator.MonotonicGenerator.Make(t time.Time) (ULID, error)— Generate a strictly monotonic ULID.
- Stateless by Default:
Make()andMakeCustom()are pure functions. No global state. - Explicit Control: You provide the time and entropy source. No hidden syscalls.
- Zero-Allocation Formatting:
AppendText()avoids heap allocations during string conversion. - Standard Library Only: No external dependencies. Uses only
crypto/rand,io, andtime. - Lexicographically Sortable: Naturally sorted by timestamp, then by entropy.
- Human-Readable Encoding: Crockford Base32 with aliases for error tolerance.
In high-throughput scenarios where multiple IDs might be generated in the same millisecond, monotonic generation ensures a guaranteed sort order without relying on wall-clock precision. This is useful for:
- Distributed event logging
- High-frequency database inserts
- Transaction IDs in concurrent systems
Use MonotonicGenerator when you need deterministic ordering, and plain Make() for simplicity when millisecond-level sorting is sufficient.