sulid

package module
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Mar 20, 2025 License: Apache-2.0 Imports: 11 Imported by: 0

README

Shorter ULID

A fork from oklog/ulid, but shorter.

Differences

  1. The random field of the ULID is 80 bits.
    The random field of the SULID is 48 bits.

    I cut the last 32 bits of the random field, which will save 6 bytes of memory space. (The length of the ULID is 26 and the length of the SULID is 20.)
    I think that a collision probability of 1/(2^48) per millisecond can still cover most scenarios.

  2. Default random number generation of ULID uses math/rand.
    Default random number generation of SULID uses math/rand/v2.

    Generating stronger random sequences using the Chacha8 algorithm from math/rand/v2 with tiny performance loss.

    Overall, ChaCha8Rand is slower than the Go 1 generator, but it is never more than twice as slow, and on typical servers the difference is never more than 3ns. Very few programs will be bottlenecked by this difference, and many programs will enjoy the improved security.

    Secure Randomness in Go 1.22

Binary Layout and Byte Order of ULID

The components are encoded as 16 octets. Each component is encoded with the Most Significant Byte first (network byte order).

        0               1               2               3
 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      32_bit_uint_time_high                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     16_bit_uint_time_low      |       16_bit_uint_random      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       32_bit_uint_random                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       32_bit_uint_random                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Binary Layout and Byte Order of SULID

The components are encoded as 12 octets. Each component is encoded with the Most Significant Byte first (network byte order).

        0               1               2               3
 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      32_bit_uint_time_high                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     16_bit_uint_time_low      |       16_bit_uint_random      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       32_bit_uint_random                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Install

This package requires Go modules.

go get -u github.com/cyberxnomad/sulid

Specification

Below is the current specification of SULID as implemented in this repository.

Components

Timestamp

  • 48 bits
  • UNIX-time in milliseconds
  • Won't run out of space till the year 10889 AD

Entropy

  • 48 bits
  • User defined entropy source
  • Monotonicity within the same millisecond with sulid.Monotonic
Encoding

Crockford's Base32 is used as shown. This alphabet excludes the letters I, L, O, and U to avoid confusion and abuse.

0123456789ABCDEFGHJKMNPQRSTVWXYZ
String Representation
 0000XSNJG0      55WMAVS5Z8
|----------|    |----------|
 Timestamp        Entropy
  10 chars        10 chars
   48bits          48bits
   base32          base32

Test

go test ./...

Documentation

Index

Examples

Constants

View Source
const EncodedSize = 20

EncodedSize is the length of a text encoded SULID.

View Source
const Encoding = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"

Encoding is the base 32 encoding alphabet used in SULID strings.

Variables

View Source
var (
	// ErrDataSize is returned when parsing or unmarshaling SULIDs with the wrong
	// data size.
	ErrDataSize = errors.New("sulid: bad data size when unmarshaling")

	// ErrInvalidCharacters is returned when parsing or unmarshaling SULIDs with
	// invalid Base32 encodings.
	ErrInvalidCharacters = errors.New("sulid: bad data characters when unmarshaling")

	// ErrBufferSize is returned when marshalling SULIDs to a buffer of insufficient
	// size.
	ErrBufferSize = errors.New("sulid: bad buffer size when marshaling")

	// ErrBigTime is returned when constructing a SULID with a time that is larger
	// than MaxTime.
	ErrBigTime = errors.New("sulid: time too big")

	// ErrOverflow is returned when unmarshaling a SULID whose first character is
	// larger than 7, thereby exceeding the valid bit depth of 128.
	ErrOverflow = errors.New("sulid: overflow when unmarshaling")

	// ErrMonotonicOverflow is returned by a Monotonic entropy source when
	// incrementing the previous SULID's entropy bytes would result in overflow.
	ErrMonotonicOverflow = errors.New("sulid: monotonic entropy overflow")

	// ErrScanValue is returned when the value passed to scan cannot be unmarshaled
	// into the SULID.
	ErrScanValue = errors.New("sulid: source value must be a string or byte slice")

	// Zero is a zero-value SULID.
	Zero SULID
)

Functions

func DefaultEntropy

func DefaultEntropy() io.Reader

DefaultEntropy returns a thread-safe per process monotonically increasing entropy source.

func MaxTime

func MaxTime() uint64

MaxTime returns the maximum Unix time in milliseconds that can be encoded in a SULID.

func Now

func Now() uint64

Now is a convenience function that returns the current UTC time in Unix milliseconds. Equivalent to:

Timestamp(time.Now().UTC())

func Time

func Time(ms uint64) time.Time

Time converts Unix milliseconds in the format returned by the Timestamp function to a time.Time.

func Timestamp

func Timestamp(t time.Time) uint64

Timestamp converts a time.Time to Unix milliseconds.

Because of the way SULID stores time, times from the year 10889 produces undefined results.

Types

type LockedMonotonicReader

type LockedMonotonicReader struct {
	MonotonicReader
	// contains filtered or unexported fields
}

LockedMonotonicReader wraps a MonotonicReader with a sync.Mutex for safe concurrent use.

func (*LockedMonotonicReader) MonotonicRead

func (r *LockedMonotonicReader) MonotonicRead(ms uint64, p []byte) (err error)

MonotonicRead synchronizes calls to the wrapped MonotonicReader.

type MonotonicEntropy

type MonotonicEntropy struct {
	io.Reader
	// contains filtered or unexported fields
}

MonotonicEntropy is an opaque type that provides monotonic entropy.

func Monotonic

func Monotonic(entropy io.Reader, inc uint32) *MonotonicEntropy

Monotonic returns a source of entropy that yields strictly increasing entropy bytes, to a limit governeed by the `inc` parameter.

Specifically, calls to MonotonicRead within the same SULID timestamp return entropy incremented by a random number between 1 and `inc` inclusive. If an increment results in entropy that would overflow available space, MonotonicRead returns ErrMonotonicOverflow.

Passing `inc == 0` results in the reasonable default `math.MaxUint16`. Lower values of `inc` provide more monotonic entropy in a single millisecond, at the cost of easier "guessability" of generated SULIDs. If your code depends on SULIDs having secure entropy bytes, then it's recommended to use the secure default value of `inc == 0`, unless you know what you're doing.

The provided entropy source must actually yield random bytes. Otherwise, monotonic reads are not guaranteed to terminate, since there isn't enough randomness to compute an increment number.

The returned type isn't safe for concurrent use.

func (*MonotonicEntropy) MonotonicRead

func (m *MonotonicEntropy) MonotonicRead(ms uint64, entropy []byte) (err error)

MonotonicRead implements the MonotonicReader interface.

type MonotonicReader

type MonotonicReader interface {
	io.Reader
	MonotonicRead(ms uint64, p []byte) error
}

MonotonicReader is an interface that should yield monotonically increasing entropy into the provided slice for all calls with the same ms parameter. If a MonotonicReader is provided to the New constructor, its MonotonicRead method will be used instead of Read.

type SULID

type SULID [12]byte

A SULID is a 12 byte Universally Unique Lexicographically Sortable Identifier

The components are encoded as 12 octets.
Each component is encoded with the MSB first (network byte order).

0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      32_bit_uint_time_high                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     16_bit_uint_time_low      |       16_bit_uint_random      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       32_bit_uint_random                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Example
package main

import (
	"encoding/binary"
	"fmt"
	"io"

	mrand "math/rand/v2"
	"time"

	"github.com/cyberxnomad/sulid"
)

func newMathV2Rng(t time.Time) io.Reader {
	seed := [32]byte{}
	binary.LittleEndian.PutUint64(seed[24:], uint64(t.UnixNano()))

	return mrand.NewChaCha8(seed)
}

func main() {
	t := time.Unix(1000000, 0)
	entropy := sulid.Monotonic(newMathV2Rng(t), 0)
	fmt.Println(sulid.MustNew(sulid.Timestamp(t), entropy))
}
Output:

0000XSNJG05BSJZW5PA8

func Make

func Make() (id SULID)

Make returns a SULID with the current time in Unix milliseconds and monotonically increasing entropy for the same millisecond. It is safe for concurrent use, leveraging a sync.Pool underneath for minimal contention.

func MustNew

func MustNew(ms uint64, entropy io.Reader) SULID

MustNew is a convenience function equivalent to New that panics on failure instead of returning an error.

func MustNewDefault

func MustNewDefault(t time.Time) SULID

MustNewDefault is a convenience function equivalent to MustNew with DefaultEntropy as the entropy. It may panic if the given time.Time is too large or too small.

func MustParse

func MustParse(sulid string) SULID

MustParse is a convenience function equivalent to Parse that panics on failure instead of returning an error.

func MustParseStrict

func MustParseStrict(sulid string) SULID

MustParseStrict is a convenience function equivalent to ParseStrict that panics on failure instead of returning an error.

func New

func New(ms uint64, entropy io.Reader) (id SULID, err error)

New returns a SULID with the given Unix milliseconds timestamp and an optional entropy source. Use the Timestamp function to convert a time.Time to Unix milliseconds.

ErrBigTime is returned when passing a timestamp bigger than MaxTime. Reading from the entropy source may also return an error.

Safety for concurrent use is only dependent on the safety of the entropy source.

func Parse

func Parse(sulid string) (id SULID, err error)

Parse parses an encoded SULID, returning an error in case of failure.

ErrDataSize is returned if the len(sulid) is different from an encoded SULID's length. Invalid encodings produce undefined SULIDs. For a version that returns an error instead, see ParseStrict.

func ParseStrict

func ParseStrict(sulid string) (id SULID, err error)

ParseStrict parses an encoded SULID, returning an error in case of failure.

It is like Parse, but additionally validates that the parsed SULID consists only of valid base32 characters. It is slightly slower than Parse.

ErrDataSize is returned if the len(sulid) is different from an encoded SULID's length. Invalid encodings return ErrInvalidCharacters.

func (SULID) Bytes

func (id SULID) Bytes() []byte

Bytes returns bytes slice representation of SULID.

func (SULID) Compare

func (id SULID) Compare(other SULID) int

Compare returns an integer comparing id and other lexicographically. The result will be 0 if id==other, -1 if id < other, and +1 if id > other.

func (SULID) Entropy

func (id SULID) Entropy() []byte

Entropy returns the entropy from the SULID.

func (SULID) IsZero

func (id SULID) IsZero() bool

IsZero returns true if the SULID is a zero-value SULID, i.e. sulid.Zero.

func (SULID) MarshalBinary

func (id SULID) MarshalBinary() ([]byte, error)

MarshalBinary implements the encoding.BinaryMarshaler interface by returning the SULID as a byte slice.

func (SULID) MarshalBinaryTo

func (id SULID) MarshalBinaryTo(dst []byte) error

MarshalBinaryTo writes the binary encoding of the SULID to the given buffer. ErrBufferSize is returned when the len(dst) != 12.

func (SULID) MarshalText

func (id SULID) MarshalText() ([]byte, error)

MarshalText implements the encoding.TextMarshaler interface by returning the string encoded SULID.

func (SULID) MarshalTextTo

func (id SULID) MarshalTextTo(dst []byte) error

MarshalTextTo writes the SULID as a string to the given buffer. ErrBufferSize is returned when the len(dst) != 20.

func (*SULID) Scan

func (id *SULID) Scan(src interface{}) error

Scan implements the sql.Scanner interface. It supports scanning a string or byte slice.

func (*SULID) SetEntropy

func (id *SULID) SetEntropy(e []byte) error

SetEntropy sets the SULID entropy to the passed byte slice. ErrDataSize is returned if len(e) != 6.

func (*SULID) SetTime

func (id *SULID) SetTime(ms uint64) error

SetTime sets the time component of the SULID to the given Unix time in milliseconds.

func (SULID) String

func (id SULID) String() string

String returns a lexicographically sortable string encoded SULID (20 characters, non-standard base 32) e.g. 01AN4Z07BY79KA1307SR9X4MV3. Format: tttttttttteeeeeeeeee where t is time and e is entropy.

func (SULID) Time

func (id SULID) Time() uint64

Time returns the Unix time in milliseconds encoded in the SULID. Use the top level Time function to convert the returned value to a time.Time.

func (SULID) Timestamp

func (id SULID) Timestamp() time.Time

Timestamp returns the time encoded in the SULID as a time.Time.

func (*SULID) UnmarshalBinary

func (id *SULID) UnmarshalBinary(data []byte) error

UnmarshalBinary implements the encoding.BinaryUnmarshaler interface by copying the passed data and converting it to a SULID. ErrDataSize is returned if the data length is different from SULID length.

func (*SULID) UnmarshalText

func (id *SULID) UnmarshalText(v []byte) error

UnmarshalText implements the encoding.TextUnmarshaler interface by parsing the data as string encoded SULID.

ErrDataSize is returned if the len(v) is different from an encoded SULID's length. Invalid encodings produce undefined SULIDs.

func (SULID) Value

func (id SULID) Value() (driver.Value, error)

Value implements the sql/driver.Valuer interface, returning the SULID as a slice of bytes, by invoking MarshalBinary. If your use case requires a string representation instead, you can create a wrapper type that calls String() instead.

type stringValuer sulid.SULID

func (v stringValuer) Value() (driver.Value, error) {
    return sulid.SULID(v).String(), nil
}

// Example usage.
db.Exec("...", stringValuer(id))

All valid SULIDs, including zero-value SULIDs, return a valid Value with a nil error. If your use case requires zero-value SULIDs to return a non-nil error, you can create a wrapper type that special-cases this behavior.

var zeroValueSULID sulid.SULID

type invalidZeroValuer sulid.SULID

func (v invalidZeroValuer) Value() (driver.Value, error) {
    if sulid.SULID(v).Compare(zeroValueSULID) == 0 {
        return nil, fmt.Errorf("zero value")
    }
    return sulid.SULID(v).Value()
}

// Example usage.
db.Exec("...", invalidZeroValuer(id))

Directories

Path Synopsis
cmd
sulid command

Jump to

Keyboard shortcuts

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