Help us shape the future of Go! Take the 2026 Go Developer Survey

ulid

package
v0.32.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 25, 2026 License: MIT Imports: 4 Imported by: 0

Documentation

Overview

Package ulid generates and validates ULIDs with the standard library only: 128-bit identifiers whose 26-character text form sorts by creation time.

g := ulid.NewGenerator()      // one per app, wired like any other service
id := g.New()                 // "01KAB2M4R1R4Q5WJ2B6PM9NZ3K"
if !ulid.Valid(r.PathValue("id")) { ... }

A ULID is the key to reach for when a row's id is handed to clients: a sequential integer leaks how many rows a table holds, and a random version 4 UUID leaks nothing but destroys index locality and needs a separate column to order by. A ULID keeps the ordering in the key: the first 48 bits are a Unix timestamp in milliseconds, the remaining 80 come from crypto/rand. The timestamp is visible to anyone holding the id (TimeOf).

An id is a plain string, so it needs no driver.Valuer or sql.Scanner and works as it is in a model field, a URL and a query. The matching column is CHAR(26), which schema.Table.ULID creates.

There is deliberately no package-level generator: staying monotonic needs state, and package-level mutable state is state nobody owns and no test can isolate. Callers hold a Generator, which also makes the clock injectable through Generator.At.

See https://github.com/AlexGalhardo/vukem/blob/main/docs/database.md#ulids.

Example
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/AlexGalhardo/vukem/database"
	"github.com/AlexGalhardo/vukem/orm"
	"github.com/AlexGalhardo/vukem/ulid"
)

// Conversion keys its rows by a ULID instead of embedding orm.Model, so the
// key can be handed to clients without leaking how many rows the table
// holds and still orders newest-last without a second column.
type Conversion struct {
	ID        string    `db:"id"`
	Name      string    `db:"name"`
	CreatedAt time.Time `db:"created_at"`
	UpdatedAt time.Time `db:"updated_at"`
}

func main() {
	ctx := context.Background()

	db, _ := database.Open(ctx, ":memory:")
	defer db.Close()

	_, _ = db.ExecContext(ctx, `CREATE TABLE conversions (id CHAR(26) PRIMARY KEY,
		name TEXT, created_at TIMESTAMP, updated_at TIMESTAMP)`)

	// One generator per app, wired like any other service: it owns the
	// state that keeps ids made in the same millisecond in order.
	ids := ulid.NewGenerator()

	// orm.Create inserts a string key exactly as given, so the caller sets
	// it; only an integer key comes back from the database.
	c := Conversion{ID: ids.New(), Name: "report.docx"}
	if err := orm.Create(ctx, db, &c); err != nil {
		fmt.Println(err)
		return
	}

	got, err := orm.QueryOf[Conversion](db).Find(ctx, c.ID)
	if err != nil {
		fmt.Println(err)
		return
	}

	at, ok := ulid.TimeOf(got.ID)

	fmt.Println(got.Name, len(got.ID), ulid.Valid(got.ID), ok, !at.After(got.CreatedAt))
	fmt.Println(ulid.Valid("not-an-id"))
}
Output:
report.docx 26 true true true
false

Index

Examples

Constants

View Source
const Length = 26

Length is the number of characters in the text form of a ULID.

Variables

This section is empty.

Functions

func TimeOf

func TimeOf(s string) (time.Time, bool)

TimeOf returns the millisecond an id was stamped with, in UTC, and whether s was a valid id. The time is advisory: it is the clock of whichever process made the id, not a verified fact.

func Valid

func Valid(s string) bool

Valid reports whether s is an id in the canonical form Generator.New returns: Length Crockford base32 characters, upper case, whose value fits 128 bits. Use it before putting a client-supplied id in a query — a value that cannot be an id does not need a database round trip to be refused.

Lower case is rejected on purpose, although the ULID specification lets a parser accept it: a CHAR(26) key must have exactly one text form, or two spellings of one id become two rows. The validation rule z.String().ULID() is the case-insensitive one, for ids this package did not make.

Types

type Generator

type Generator struct {
	// contains filtered or unexported fields
}

Generator hands out ULIDs that are unique and strictly increasing, even for several ids made within the same millisecond. It is safe for concurrent use and its zero value is ready to use.

func NewGenerator

func NewGenerator() *Generator

NewGenerator returns a generator ready for use.

func (*Generator) At

func (g *Generator) At(t time.Time) string

At returns an id stamped with t, clamped to the range the 48-bit timestamp holds (the Unix epoch to year 10889) rather than silently encoded as some other time.

When t lands in a millisecond already used, the random tail is incremented instead of redrawn, which keeps the ids strictly increasing. The same happens when t is before the last id: a clock that steps backwards must not produce an id that sorts before one already handed out.

func (*Generator) New

func (g *Generator) New() string

New returns an id stamped with the current time.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL