random

random provides utility functions for generating random bytes, numeric identifiers, UID/UUID values, hexadecimal/base36 IDs, and configurable random strings.

Part of nurago, a collection of independent Go packages for backend services.

import "github.com/tecnickcom/nurago/pkg/random"

Package random provides utility functions for generating random bytes, numeric identifiers, UID/UUID values, hexadecimal/base36 IDs, and configurable random strings.

Randomness Source

By default, New uses crypto/rand.Reader, which is suitable for security-sensitive randomness. A custom io.Reader can be supplied directly to New for testing or specialized environments.

The non-failing helpers (Rnd.RandUint32, Rnd.RandUint64 and Rnd.UUIDv7, plus Rnd.RandHex64, Rnd.RandString64, Rnd.UID64 and Rnd.UID128, which are built on them) fall back to math/rand/v2 if the reader fails, so that their signatures can stay error-free. Rnd.RandomBytes and Rnd.RandString never fall back: they return the reader’s error.

With the default crypto/rand.Reader the fallback is unreachable, because its Read cannot return an error. It is reachable for a caller-supplied reader, and it is silent by default: the entropy source is swapped without an error and without a signal. Register WithFallbackHook to observe it. The fallback draws from Go’s OS-seeded ChaCha8 global source, so the output is not predictable, but it is no longer the source the caller configured.

A reader that never makes progress (one that keeps returning zero bytes with a nil error, or whose bytes are never usable) is not retried forever: the helpers give up and report ErrReaderNoProgress rather than hanging.

What It Provides

  • Rnd.RandomBytes for raw random byte slices.
  • Rnd.RandUint32 and Rnd.RandUint64 for random integers.
  • Rnd.RandHex64 for fixed-length 16-char hexadecimal IDs.
  • Rnd.RandString64 for compact base-36 IDs.
  • Rnd.RandString for random strings of length n using a configurable byte-to-character map.
  • Rnd.UID64 for time-aware 64-bit unique identifiers (TUID64) with TUID64.Hex and TUID64.String formats.
  • Rnd.UID128 for time+random 128-bit unique identifiers (TUID128) with TUID128.Hex and TUID128.String formats.
  • Rnd.UUIDv7 for RFC 9562 UUID version 7 values (UUID).
  • UUID.Format, TUID64.Format and TUID128.Format to write the textual form into a caller-owned buffer.

Character Map Customization

Rnd.RandString uses a default map containing digits, uppercase/lowercase letters, and symbols. You can override it with WithByteToCharMap.

The map is a map of bytes, not of runes: each output position is filled with one byte drawn from it. Entries must therefore be single-byte (ASCII) values.

  • Multi-byte UTF-8 runes are not supported. A map containing them is not rejected, but its runes are split into their constituent bytes, which are then drawn independently, so the result is almost always invalid UTF-8.
  • Empty map input restores the default map.
  • Maps longer than 256 bytes are truncated to 256.
  • A single-entry map yields a constant string with no entropy.

Performance

Generation cost is dominated by two largely irreducible operations: reading the wall clock and reading entropy from the configured reader. Each call makes one small heap allocation for the random bytes, which is structural: the read goes through an io.Reader, so the buffer cannot stay on the stack. Rnd.UUIDv7 draws only the 8 random bytes it needs for the 62-bit rand_b field rather than a full 16, so it reads half the entropy a naive v7 construction does.

The generators hold no shared mutable state and take no locks, so a single Rnd is safe for concurrent use without serializing callers and concurrent throughput is bounded by the reader. Ordering of values generated within the same sub-millisecond instant is statistical rather than strictly monotonic, which is the trade that keeps the path lock-free.

Textual formatting is table-driven through uhex instead of reflection-based formatting: each byte is translated with a single lookup into a 256-entry table, unrolled and branch-free.

  • The Format methods write into a caller-owned array and allocate nothing. For the UUID case this is well over an order of magnitude faster than fmt.Sprintf("%x-%x-%x-%x-%x", ...), and roughly two to three times faster than encoding/hex plus manual separator insertion.
  • String and Hex layer a single result allocation on top of Format.
  • Byte fills a local array and returns a slice over it, so it allocates only when the result escapes the caller.

These are relative characteristics, not guarantees; absolute numbers depend on the hardware, the compiler, and the configured reader. The package ships benchmarks (run with go test -bench=.) so the figures can be reproduced on the target platform.

Usage

r := random.New(nil) // default: crypto/rand.Reader

id := r.RandHex64()
short := r.RandString64()
_ = id
_ = short

pwd, err := r.RandString(24)
if err != nil {
    return err
}
_ = pwd

uid64 := r.UID64()
uid128 := r.UID128()
uuid := r.UUIDv7()
_ = uid64.Hex()
_ = uid128.String()
_ = uuid.String()

alphaNum := []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
r2 := random.New(nil, random.WithByteToCharMap(alphaNum))
_, _ = r2.RandString(16)

When To Use

  • You need UUIDs, hex IDs, or base36 IDs without adding a dedicated dependency.
  • Random strings must be drawn from a specified alphabet.

Example

r := random.New(nil)

b, err := r.RandomBytes(4)
if err != nil {
	log.Fatal(err)
}

fmt.Printf("%v\n", b)

Full source is in example_random_test.go. More runnable examples are on pkg.go.dev.

Dependencies

This package reaches no external module: it uses only the Go standard library.