typeid

package module
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: Mar 24, 2026 License: MIT Imports: 7 Imported by: 0

README

typeid

Prefixed, base32-encoded, k-sortable identifiers for Go. Inspired by Stripe API IDs and the TypeID spec.

Identifier format

user_01kmfjypewe1wrfeb01wjfxand       UUID  — 26-char suffix
└──┘ └────────────────────────┘
type   Crockford base32

org_01kmfjypewdwg                     Int64 — 13-char suffix
└─┘ └───────────┘
type  Crockford base32

The alphabet is Crockford base32 (lowercase) and excludes ambiguous characters: i, l, o, u.

Two flavours

Both are UUIDv7-based and sort by creation time (no UUIDv4 — sortability gives good DB locality and time-ordered IDs).

Type Backing Postgres Suffix When to use
UUID[P] 128 bit uuid 26 chars Any throughput. Users, events, logs — use by default.
Int64[P] 63 bit BIGINT 13 chars <~100 IDs/sec. Orgs, tenants — compact IDs, 15 random bits; use UNIQUE + retry on conflict.

Usage

Define typed IDs
import "github.com/go-chi/typeid"

type userPrefix struct{}
func (userPrefix) Prefix() string { return "user" }

type UserID = typeid.UUID[userPrefix]

type orgPrefix struct{}
func (orgPrefix) Prefix() string { return "org" }

type OrgID = typeid.Int64[orgPrefix]
Create new IDs
userID, err := typeid.NewUUID[userPrefix]()   // user_01kmfjypewe1wrfeb01wjfxand
orgID,  err := typeid.NewInt64[orgPrefix]()   // org_01kmfjypewdwg
Parse from string
id, err := typeid.ParseUUID[userPrefix]("user_01kmfjypewe1wrfeb01wjfxand")
id, err := typeid.ParseInt64[orgPrefix]("org_01kmfjypewdwg")

Parsing validates the prefix at compile time — passing "org_..." to ParseUUID[userPrefix] returns an error.

Wrap raw values
id, err := typeid.UUIDFrom[userPrefix](rawUUID)   // rejects non-UUIDv7
id, err := typeid.Int64From[orgPrefix](rawInt64)   // rejects non-positive
Use in structs
type User struct {
    ID   UserID `json:"id"`
    Name string `json:"name"`
}

type Org struct {
    ID   OrgID  `json:"id"`
    Name string `json:"name"`
}

Serialisation

Both types implement:

Interface Behaviour
fmt.Stringer "prefix_base32suffix"
encoding.TextMarshaler / TextUnmarshaler Same text form (JSON uses this automatically)
driver.Valuer UUID[P] → UUID string, Int64[P]int64
sql.Scanner UUID[P]string/[]byte/[16]byte, Int64[P]int64

Int64 bit layout

[48-bit unix ms timestamp][15-bit crypto/rand] = 63 bits, always positive

Stored as Postgres BIGINT. Collision table: 10 IDs/sec → ~1 per 7,500 days; 100/sec → ~1 per 1.8 hours; 1,000/sec → ~1 per 65 seconds.

Benchmarks

Apple M4 Pro, Go 1.26.1:

BenchmarkInt64_String         ~19 ns/op    24 B/op    1 allocs/op
BenchmarkInt64_MarshalText    ~18 ns/op    24 B/op    1 allocs/op
BenchmarkInt64_Parse          ~18 ns/op     0 B/op    0 allocs/op
BenchmarkUUID_String          ~24 ns/op    32 B/op    1 allocs/op
BenchmarkUUID_MarshalText     ~23 ns/op    32 B/op    1 allocs/op
BenchmarkUUID_Parse           ~33 ns/op     0 B/op    0 allocs/op

Parse is zero-allocation. Encode paths do a single allocation for the output buffer.

License

MIT License

Documentation

Overview

Example
package main

import (
	"database/sql"
	"database/sql/driver"
	"encoding"
	"fmt"
	"strings"

	"github.com/go-chi/typeid"
)

// Prefix definitions — in practice these live next to each domain entity.

type userPrefix struct{}

func (userPrefix) Prefix() string { return "user" }

type orgPrefix struct{}

func (orgPrefix) Prefix() string { return "org" }

// Type aliases give readable names.
type (
	UserID = typeid.UUID[userPrefix]
	OrgID  = typeid.Int64[orgPrefix]
)

// Compile-time interface checks.
var (
	_ fmt.Stringer             = UserID{}
	_ fmt.Stringer             = OrgID{}
	_ encoding.TextMarshaler   = UserID{}
	_ encoding.TextMarshaler   = OrgID{}
	_ encoding.TextUnmarshaler = (*UserID)(nil)
	_ encoding.TextUnmarshaler = (*OrgID)(nil)
	_ driver.Valuer            = UserID{}
	_ driver.Valuer            = OrgID{}
	_ sql.Scanner              = (*UserID)(nil)
	_ sql.Scanner              = (*OrgID)(nil)
)

func main() {
	orgID, err := typeid.NewInt64[orgPrefix]()
	if err != nil {
		panic(err)
	}

	userID, err := typeid.NewUUID[userPrefix]()
	if err != nil {
		panic(err)
	}

	fmt.Println(strings.HasPrefix(orgID.String(), "org_"))
	fmt.Println(strings.HasPrefix(userID.String(), "user_"))
}
Output:
true
true

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrOnlyV7         = errors.New("typeid: only UUIDv7 is supported")
	ErrZeroUUID       = errors.New("typeid: zero UUID")
	ErrNonPositiveInt = errors.New("typeid: non-positive Int64")
	ErrOverflowBase32 = errors.New("typeid: base32 overflow at pos 0")
	ErrOverflowInt64  = errors.New("typeid: value overflows int64")
)

Functions

This section is empty.

Types

type Int64

type Int64[P Prefixer] struct {
	// contains filtered or unexported fields
}

Int64 is a type-safe compact identifier. Maps to Postgres BIGINT.

Bit layout

[48-bit unix ms timestamp][15-bit crypto/rand] = 63 bits, always positive

Timestamp range

48-bit millisecond timestamp (same as UUIDv7) covers Unix epoch through year 10889. No action needed in our lifetimes.

Collision resistance

15 random bits = 32,768 values per millisecond. Collision probability follows the birthday problem: ~R²/65,536,000 expected collisions per second for R total IDs/sec across all servers.

   10 IDs/sec → ~1 collision per 7,500 days
  100 IDs/sec → ~1 collision per 1.8 hours
1,000 IDs/sec → ~1 collision per 65 seconds

Protect with a UNIQUE constraint and retry on conflict. For high-throughput resources use UUID instead.

Ordering (k-sortable)

IDs are k-sortable: the 48-bit timestamp in the high bits dominates sort order, so IDs sort by creation time at millisecond granularity. Two IDs generated in the exact same millisecond are not ordered relative to each other, but they cluster on the same B-tree leaf pages — no impact on Postgres insert locality. Clock skew between servers may produce out-of-order IDs within that skew window.

Example (Json)
type Org struct {
	ID   OrgID  `json:"id"`
	Name string `json:"name"`
}

id, _ := typeid.NewInt64[orgPrefix]()
original := Org{ID: id, Name: "Polygon"}
data, _ := json.Marshal(original)

var decoded Org
_ = json.Unmarshal(data, &decoded)
fmt.Println(original.ID == decoded.ID)
fmt.Println(strings.Contains(string(data), `"id":"org_`))
Output:
true
true

func Int64From

func Int64From[P Prefixer](v int64) (Int64[P], error)
Example
id, _ := typeid.NewInt64[orgPrefix]()
raw := id.Int64()
reconstructed, err := typeid.Int64From[orgPrefix](raw)
if err != nil {
	fmt.Println("error:", err)
	return
}
fmt.Println(id == reconstructed)
Output:
true
Example (RejectsNonPositive)
_, err := typeid.Int64From[orgPrefix](-1)
fmt.Println(err)
_, err = typeid.Int64From[orgPrefix](0)
fmt.Println(err)
Output:
typeid: non-positive Int64
typeid: non-positive Int64

func NewInt64

func NewInt64[P Prefixer]() (Int64[P], error)
Example
id, err := typeid.NewInt64[orgPrefix]()
if err != nil {
	fmt.Println("error:", err)
	return
}
s := id.String()

prefix, suffix, _ := strings.Cut(s, "_")
fmt.Println(prefix)
fmt.Println(len(suffix))
fmt.Println(id.Int64() > 0)
Output:
org
13
true

func ParseInt64

func ParseInt64[P Prefixer](s string) (Int64[P], error)
Example
original, _ := typeid.NewInt64[orgPrefix]()
parsed, err := typeid.ParseInt64[orgPrefix](original.String())
if err != nil {
	fmt.Println("error:", err)
	return
}
fmt.Println(original == parsed)
Output:
true
Example (WrongPrefix)
_, err := typeid.ParseInt64[orgPrefix]("foo_0h455vb4pex5v")
fmt.Println(err)
Output:
typeid: prefix mismatch: expected "org", got "foo"

func (Int64[P]) Int64

func (id Int64[P]) Int64() int64

func (Int64[P]) IsZero

func (id Int64[P]) IsZero() bool
Example
var id OrgID
fmt.Println(id.IsZero())
id, _ = typeid.NewInt64[orgPrefix]()
fmt.Println(id.IsZero())
Output:
true
false

func (Int64[P]) MarshalText

func (id Int64[P]) MarshalText() ([]byte, error)

func (*Int64[P]) Scan

func (id *Int64[P]) Scan(src any) error
Example
id, _ := typeid.NewInt64[orgPrefix]()
raw := id.Int64()

var scanned OrgID
err := scanned.Scan(raw)
fmt.Println(err == nil)
fmt.Println(id == scanned)
Output:
true
true

func (Int64[P]) String

func (id Int64[P]) String() string

func (*Int64[P]) UnmarshalText

func (id *Int64[P]) UnmarshalText(data []byte) error

func (Int64[P]) Value

func (id Int64[P]) Value() (driver.Value, error)
Example
id, _ := typeid.NewInt64[orgPrefix]()
val, _ := id.Value()
v, ok := val.(int64)
fmt.Println(ok)
fmt.Println(v > 0)
Output:
true
true

type Prefixer

type Prefixer interface {
	Prefix() string
}

Prefixer is the constraint for type-safe ID prefixes.

type UUID

type UUID[P Prefixer] struct {
	// contains filtered or unexported fields
}

UUID is a type-safe UUIDv7 identifier with a compile-time prefix. Maps to Postgres uuid.

Example (Json)
type User struct {
	ID   UserID `json:"id"`
	Name string `json:"name"`
}

id, _ := typeid.NewUUID[userPrefix]()
original := User{ID: id, Name: "Alice"}
data, _ := json.Marshal(original)

var decoded User
_ = json.Unmarshal(data, &decoded)
fmt.Println(original.ID == decoded.ID)
fmt.Println(strings.Contains(string(data), `"id":"user_`))
Output:
true
true

func NewUUID

func NewUUID[P Prefixer]() (UUID[P], error)
Example
id, err := typeid.NewUUID[userPrefix]()
if err != nil {
	fmt.Println("error:", err)
	return
}
s := id.String()

prefix, suffix, _ := strings.Cut(s, "_")
fmt.Println(prefix)
fmt.Println(len(suffix))
fmt.Println(int(id.UUID().Version()))
Output:
user
26
7

func ParseUUID

func ParseUUID[P Prefixer](s string) (UUID[P], error)
Example
original, _ := typeid.NewUUID[userPrefix]()
parsed, err := typeid.ParseUUID[userPrefix](original.String())
if err != nil {
	fmt.Println("error:", err)
	return
}
fmt.Println(original == parsed)
Output:
true
Example (WrongPrefix)
_, err := typeid.ParseUUID[userPrefix]("team_01h455vb4pex5vsknk084sn02q")
fmt.Println(err)
Output:
typeid: prefix mismatch: expected "user", got "team"

func UUIDFrom

func UUIDFrom[P Prefixer](u uuid.UUID) (UUID[P], error)
Example
raw := uuid.Must(uuid.NewV7())
id, err := typeid.UUIDFrom[userPrefix](raw)
if err != nil {
	fmt.Println("error:", err)
	return
}
fmt.Println(id.UUID() == raw)
Output:
true
Example (RejectsV4)
v4 := uuid.New()
_, err := typeid.UUIDFrom[userPrefix](v4)
fmt.Println(err)
Output:
typeid: only UUIDv7 is supported

func (UUID[P]) IsZero

func (id UUID[P]) IsZero() bool
Example
var id UserID
fmt.Println(id.IsZero())
id, _ = typeid.NewUUID[userPrefix]()
fmt.Println(id.IsZero())
Output:
true
false

func (UUID[P]) MarshalText

func (id UUID[P]) MarshalText() ([]byte, error)

func (*UUID[P]) Scan

func (id *UUID[P]) Scan(src any) (err error)
Example
id, _ := typeid.NewUUID[userPrefix]()
raw := id.UUID().String()

var scanned UserID
err := scanned.Scan(raw)
fmt.Println(err == nil)
fmt.Println(id == scanned)
Output:
true
true

func (UUID[P]) String

func (id UUID[P]) String() string

func (UUID[P]) UUID

func (id UUID[P]) UUID() uuid.UUID

func (*UUID[P]) UnmarshalText

func (id *UUID[P]) UnmarshalText(data []byte) error

func (UUID[P]) Value

func (id UUID[P]) Value() (driver.Value, error)
Example
id, _ := typeid.NewUUID[userPrefix]()
val, _ := id.Value()
s, ok := val.(string)
fmt.Println(ok)
_, err := uuid.Parse(s)
fmt.Println(err == nil)
Output:
true
true

Jump to

Keyboard shortcuts

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