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{}
_ fmt.Stringer = typeid.AnyUUID{}
_ fmt.Stringer = typeid.AnyInt64{}
_ encoding.TextMarshaler = UserID{}
_ encoding.TextMarshaler = OrgID{}
_ encoding.TextMarshaler = typeid.AnyUUID{}
_ encoding.TextMarshaler = typeid.AnyInt64{}
_ encoding.TextUnmarshaler = (*UserID)(nil)
_ encoding.TextUnmarshaler = (*OrgID)(nil)
_ encoding.TextUnmarshaler = (*typeid.AnyUUID)(nil)
_ encoding.TextUnmarshaler = (*typeid.AnyInt64)(nil)
_ driver.Valuer = UserID{}
_ driver.Valuer = OrgID{}
_ driver.Valuer = typeid.AnyUUID{}
_ driver.Valuer = typeid.AnyInt64{}
_ sql.Scanner = (*UserID)(nil)
_ sql.Scanner = (*OrgID)(nil)
_ sql.Scanner = (*typeid.AnyUUID)(nil)
_ sql.Scanner = (*typeid.AnyInt64)(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 ¶
- Variables
- type AnyInt64
- func (id AnyInt64) Int64() int64
- func (id AnyInt64) IsZero() bool
- func (id AnyInt64) MarshalText() ([]byte, error)
- func (id AnyInt64) Prefix() string
- func (id *AnyInt64) Scan(src any) error
- func (id *AnyInt64) SetPrefix(s string)
- func (id AnyInt64) String() string
- func (id *AnyInt64) UnmarshalText(data []byte) error
- func (id AnyInt64) Value() (driver.Value, error)
- type AnyUUID
- func (id AnyUUID) GetTime() time.Time
- func (id AnyUUID) IsZero() bool
- func (id AnyUUID) MarshalText() ([]byte, error)
- func (id AnyUUID) Prefix() string
- func (id *AnyUUID) Scan(src any) (err error)
- func (id *AnyUUID) SetPrefix(s string)
- func (id AnyUUID) String() string
- func (id AnyUUID) UUID() uuid.UUID
- func (id *AnyUUID) UnmarshalText(data []byte) error
- func (id AnyUUID) Value() (driver.Value, error)
- type Int64
- type Prefixer
- type UUID
- func (id UUID[P]) Any() AnyUUID
- func (id UUID[P]) IsZero() bool
- func (id UUID[P]) MarshalText() ([]byte, error)
- func (id *UUID[P]) Scan(src any) (err error)
- func (id UUID[P]) String() string
- func (id UUID[P]) UUID() uuid.UUID
- func (id *UUID[P]) UnmarshalText(data []byte) error
- func (id UUID[P]) Value() (driver.Value, error)
Examples ¶
- Package
- AnyInt64 (SwitchToTypedInt64)
- AnyUUID (Json)
- AnyUUID (SwitchToTypedUUID)
- Int64 (Json)
- Int64.IsZero
- Int64.Scan
- Int64.Value
- Int64From
- Int64From (RejectsNonPositive)
- NewInt64
- NewUUID
- ParseInt64
- ParseInt64 (WrongPrefix)
- ParseUUID
- ParseUUID (WrongPrefix)
- UUID (Json)
- UUID.IsZero
- UUID.Scan
- UUID.Value
- UUIDFrom
- UUIDFrom (RejectsV4)
Constants ¶
This section is empty.
Variables ¶
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 AnyInt64 ¶ added in v0.0.2
type AnyInt64 struct {
// contains filtered or unexported fields
}
AnyInt64 is a compact identifier with a runtime-configurable prefix. Unlike Int64, the prefix is not fixed at compile time.
Example (SwitchToTypedInt64) ¶
ExampleAnyInt64_switchToTypedInt64 narrows AnyInt64 to Int64 after a prefix switch.
const payload = `{"id":"org_01hf7yat00c1s"}`
type Request struct {
ID typeid.AnyInt64 `json:"id"`
}
var req Request
if err := json.Unmarshal([]byte(payload), &req); err != nil {
fmt.Println("unmarshal:", err)
return
}
var orgID OrgID
var err error
switch req.ID.Prefix() {
case "org":
orgID, err = typeid.Int64From[orgPrefix](req.ID.Int64())
default:
fmt.Println("unknown prefix")
return
}
if err != nil {
fmt.Println("narrow:", err)
return
}
fmt.Println(orgID.String())
Output: org_01hf7yat00c1s
func NewAnyInt64 ¶ added in v0.0.2
func ParseAnyInt64 ¶ added in v0.0.2
func (AnyInt64) MarshalText ¶ added in v0.0.2
func (*AnyInt64) UnmarshalText ¶ added in v0.0.2
type AnyUUID ¶ added in v0.0.2
type AnyUUID struct {
// contains filtered or unexported fields
}
AnyUUID is a UUIDv7 identifier with a runtime-configurable prefix. Unlike UUID, the prefix is not fixed at compile time.
Example (Json) ¶
package main
import (
"encoding/json"
"fmt"
"github.com/go-chi/typeid"
)
type Mode string
const (
ModeLive Mode = "key"
ModeSandbox Mode = "key_sandbox"
)
type ApiKeyID struct {
typeid.AnyUUID
}
func NewApiKeyID(mode Mode) ApiKeyID {
u, err := typeid.NewAnyUUID(string(mode))
if err != nil {
panic(err) // can't happen unless crypto/rand is not available
}
return ApiKeyID{AnyUUID: u}
}
func (id *ApiKeyID) UnmarshalText(data []byte) error {
if err := id.AnyUUID.UnmarshalText(data); err != nil {
return err
}
switch id.AnyUUID.Prefix() {
case string(ModeLive), string(ModeSandbox):
return nil
default:
return fmt.Errorf("invalid api key prefix: %q", id.AnyUUID.Prefix())
}
}
type Request struct {
ID ApiKeyID `json:"id"`
Description string `json:"description"`
}
func main() {
// Sandbox
data, _ := json.Marshal(Request{ID: NewApiKeyID(ModeSandbox), Description: "Sandbox API Key"})
var sandboxRequest Request
_ = json.Unmarshal(data, &sandboxRequest)
// Live
data, _ = json.Marshal(Request{ID: NewApiKeyID(ModeLive), Description: "Live API Key"})
var liveRequest Request
_ = json.Unmarshal(data, &liveRequest)
// Invalid prefix, expect error
data = []byte(`{"id":"key_invalid_prefix_01jcp1ss00edg828t5cy4tqkff", "description":"Invalid API Key"}`)
var unknownRequest Request
err := json.Unmarshal(data, &unknownRequest)
fmt.Println(sandboxRequest.ID.Prefix())
fmt.Println(liveRequest.ID.Prefix())
fmt.Println(unknownRequest.ID.Prefix(), err)
}
Output: key_sandbox key key_invalid_prefix invalid api key prefix: "key_invalid_prefix"
Example (SwitchToTypedUUID) ¶
ExampleAnyUUID_switchToTypedUUID shows narrowing AnyUUID to UUID after inspecting AnyUUID.Prefix. Use UUIDFrom when the prefix matches; it keeps the same UUID bytes under the typed wrapper.
const payload = `{"id":"user_01jcp1ss00edg828t5cy4tqkff"}`
type Request struct {
ID typeid.AnyUUID `json:"id"`
}
var req Request
if err := json.Unmarshal([]byte(payload), &req); err != nil {
fmt.Println("unmarshal:", err)
return
}
var userID UserID
var err error
switch req.ID.Prefix() {
case "user":
userID, err = typeid.UUIDFrom[userPrefix](req.ID.UUID())
default:
fmt.Println("unknown prefix")
return
}
if err != nil {
fmt.Println("narrow:", err)
return
}
fmt.Println(userID.String())
Output: user_01jcp1ss00edg828t5cy4tqkff
func NewAnyUUID ¶ added in v0.0.2
func ParseAnyUUID ¶ added in v0.0.2
func (AnyUUID) GetTime ¶ added in v0.0.2
GetTime extracts the millisecond-precision creation timestamp from the UUIDv7.
func (AnyUUID) MarshalText ¶ added in v0.0.2
func (*AnyUUID) UnmarshalText ¶ added in v0.0.2
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 ¶
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 ¶
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 ¶
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]) IsZero ¶
Example ¶
var id OrgID fmt.Println(id.IsZero()) id, _ = typeid.NewInt64[orgPrefix]() fmt.Println(id.IsZero())
Output: true false
func (Int64[P]) MarshalText ¶
func (*Int64[P]) Scan ¶
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]) UnmarshalText ¶
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 ¶
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 ¶
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 ¶
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]) Any ¶ added in v0.0.2
Any converts a typed UUID to an AnyUUID with the same prefix and value.
func (UUID[P]) IsZero ¶
Example ¶
var id UserID fmt.Println(id.IsZero()) id, _ = typeid.NewUUID[userPrefix]() fmt.Println(id.IsZero())
Output: true false
func (UUID[P]) MarshalText ¶
func (*UUID[P]) Scan ¶
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