Skip to content

ChrisRx/exp

Repository files navigation

exp

Go Reference Build Status

Experimental Go packages being evaluated for reusability and stability. All packages share a single Go module and are imported as go.chrisrx.dev/x/<subpackage>.

When a package's API stabilizes and proves useful, it is promoted to its own module:

// before promotion
import "go.chrisrx.dev/x/env"

// after promotion
import "go.chrisrx.dev/env"

Extended standard library packages

Several packages in this module are designed as drop-in replacements for their standard library counterparts. Swapping the import path gives you all the original stdlib functions plus additional utilities — no other code changes required.

This module Extends
go.chrisrx.dev/x/slices slices
go.chrisrx.dev/x/strings strings
go.chrisrx.dev/x/errors errors
go.chrisrx.dev/x/sync sync

This is implemented with aliaspkg, a code-generation tool that re-exports every symbol from the target stdlib package under the replacement package name. The generated file is committed as alias.go and is never edited by hand. Each re-exported function includes a doc comment linking back to the original stdlib entry so godoc references remain intact.

// Before: only stdlib slices functions available.
import "slices"

// After: all stdlib functions plus Filter, Map, FlatMap, Find, Partition, Uniq, etc.
import "go.chrisrx.dev/x/slices"

To regenerate the alias file for a package after a Go version upgrade, run:

go generate ./slices/...

Highlights

slices

A superset of the standard library slices package. It can be used as a drop-in replacement — see Extended standard library packages below — while adding generic higher-order functions missing from the stdlib:

import "go.chrisrx.dev/x/slices"

names := []string{"alice", "bob", "carol"}

// Filter keeps elements matching a predicate.
long := slices.Filter(names, func(s string) bool { return len(s) > 3 })

// Map transforms elements.
upper := slices.Map(names, strings.ToUpper)

// Partition splits into two slices based on a predicate.
short, long := slices.Partition(names, func(s string) bool { return len(s) <= 3 })

// Uniq removes duplicates (works on any type, including uncomparable ones).
unique := slices.Uniq([]string{"a", "b", "a", "c"})

// FlatMap maps then flattens.
words := slices.FlatMap(sentences, strings.Fields)

set

A generic set that works with any type, including uncomparable types like []byte, using maphash internally. The zero value is ready to use.

import "go.chrisrx.dev/x/set"

// Works with comparable types.
s := set.New(1, 2, 2, 3, 3, 3)
fmt.Println(s.List()) // [1 2 3]

// Also works with uncomparable types.
var bs set.Set[[]byte]
bs.Add([]byte("foo"), []byte("foo"), []byte("bar"))
fmt.Println(bs.Len()) // 2

// Set operations.
a := set.New("x", "y")
b := set.New("y", "z")
fmt.Println(a.Union(b).List())        // [x y z]
fmt.Println(a.Intersection(b).List()) // [y]
fmt.Println(a.Difference(b).List())   // [x]

// OrderedSet preserves insertion order and is iterable.
os := set.NewOrderedSet("c", "a", "b")
for v := range os.Values() {
    fmt.Println(v) // c, a, b
}

env

Populates a struct from environment variables using struct tags. Supports default values (including Go expression defaults via $default), expression-based validation, automatic prefix derivation from struct nesting, and custom type parsers.

import "go.chrisrx.dev/x/env"

cfg := env.MustParseFor[struct {
    Addr           string        `env:"ADDR"           default:":8080"  validate:"split_addr(self).port > 1024"`
    ReadTimeout    time.Duration `env:"TIMEOUT"        default:"2m"`
    MaxHeaderBytes int           `env:"MAX_HEADER_BYTES" $default:"1 << 20"`
}](env.RootPrefix("MYSERVICE"))
// Reads MYSERVICE_ADDR, MYSERVICE_TIMEOUT, MYSERVICE_MAX_HEADER_BYTES.

Nested structs automatically extend the prefix chain, so each level of nesting maps cleanly to a PARENT_CHILD_FIELD naming scheme without any manual configuration.

group

Manages a pool of goroutines with optional bounded concurrency, context propagation, and first-error cancellation. ResultGroup extends this to collect typed return values as an iter.Seq2 iterator, making fan-out patterns concise.

import "go.chrisrx.dev/x/group"

// Basic fan-out — cancel all goroutines on first error.
g := group.New(ctx, group.WithLimit(8))
for _, item := range items {
    g.Go(func(ctx context.Context) error {
        return process(ctx, item)
    })
}
if err := g.Wait(); err != nil {
    log.Fatal(err)
}

// Collect typed results as an iterator.
rg := group.NewResultGroup[string](ctx, group.WithLimit(4))
for _, url := range urls {
    rg.Go(func(ctx context.Context) (string, error) {
        return fetch(ctx, url)
    })
}
for result, err := range rg.Get() {
    if err != nil { ... }
    fmt.Println(result)
}

run

Retry and polling primitives built on exponential backoff. Every runs a function on a fixed interval until the context is cancelled. Until and Unless retry until a condition is met or fails. All three accept either a time.Duration or a full Options value for fine-grained backoff control.

import "go.chrisrx.dev/x/run"

// Call a function every 500ms until the context is done.
run.Every(ctx, func() {
    metrics.Collect()
}, 500*time.Millisecond)

// Retry until the function returns nil (no error).
err := run.Until(ctx, func() error {
    return db.Ping()
}, run.Options{
    InitialInterval: 100 * time.Millisecond,
    MaxInterval:     5 * time.Second,
    MaxAttempts:     10,
})

// Retry until the function returns false.
err = run.Unless(ctx, func() bool {
    return queue.Full()
}, time.Second)

must

Unwraps (T, error) return values and panic-recovery helpers. must.Ok panics on a non-nil error, eliminating boilerplate in initialization code. must.Catch converts a panic back into an error for deferred recovery. must.Recover silently absorbs panics (optionally only specific ones) and logs them.

import "go.chrisrx.dev/x/must"

// Unwrap (value, error) — panics if err != nil.
f := must.Ok(os.Open("config.yaml"))

// Catch a panic and surface it as an error return.
func parse(data []byte) (result Result, err error) {
    defer must.Catch(&err)
    result = mustParse(data) // panics on bad input
    return
}

// Recover all panics, logging them via slog.
defer must.Recover()

// Recover only a specific error.
defer must.Recover(io.ErrUnexpectedEOF)

safe

Converts panics to errors and provides struct-level compile-time guards. safe.Do wraps any func() and returns a panic as an error. The marker types NoCompare, NoCopy, and NoTypeConversion are zero-size fields that enforce invariants at compile time or via go vet.

import "go.chrisrx.dev/x/safe"

// Run untrusted code without crashing the process.
err := safe.Do(func() {
    riskyOperation()
})

// Prevent a struct from being copied after first use (triggers go vet).
type Buffer struct {
    _ safe.NoCopy
    // ...
}

// Prevent unsafe type conversion between two struct types.
type MyID struct {
    _ safe.NoTypeConversion[OtherID]
    v uint64
}

log

Wraps log/slog with environment-driven configuration. Call log.SetDefault() once at startup to read LOG_LEVEL, LOG_FORMAT (text/json), LOG_ADD_SOURCE, and LOG_REMOVE_ATTRS from the environment and install a configured default logger. log.New builds a standalone *slog.Logger from the same option set.

import "go.chrisrx.dev/x/log"

func main() {
    // Configure the default slog.Logger from environment variables once.
    log.SetDefault()

    // Build a logger with explicit options (overrides env defaults).
    logger := log.New(
        log.WithFormat(log.JSONFormat),
        log.WithLevel(slog.LevelDebug),
    )

    // Fatal variants log via slog then call os.Exit(1).
    // Set GO_BACKTRACE=1 to print the full stack trace to stderr.
    log.Fatalf("startup failed: %v", err)
}

Packages

Package Description
assert Testing assertions for values, errors, and eventual conditions with diff output
backoff Exponential backoff with configurable intervals, multipliers, and jitter
chans Channel utilities: collect, drain, and limit operations
constraints Generic numeric constraint types (Signed, Unsigned, Integer, Float, Complex)
context Type-safe context keys using generics
convert Type-safe conversions via a registry of conversion functions
env Parse environment variables into struct fields using tags
errors Drop-in errors replacement with generic As, Wrap, Ignore, and Stack
expr Parse and evaluate Go expressions at runtime using reflection
future Lazy-evaluated values with channel-based synchronization
group Goroutine pool with concurrency limits, context cancellation, and error handling
log slog.Logger construction with environment-based configuration
maps Map utilities: filter, transform, and convert to slices
math Generic math functions (e.g. Sum) for numeric types
must Unwrap error tuples, panicking on non-nil errors
options Generic option pattern with composable Option[T] interface
pagetoken Encrypted, base64-serialized pagination tokens
print Pretty-print values as formatted JSON
ptr Pointer helpers: create, dereference, nullable conversion, zero-value check
random Cryptographically secure random string generation
result Of[T] result type with iter.Seq support
run Retry with configurable exponential backoff, max attempts, and time limits
safe Execute functions safely, converting panics to errors
set Generic set supporting uncomparable values via maphash
slices Slice utilities: filter, map, find, truncate
sort Sorted map view with pagination and iteration
stack Caller info from the runtime, filtering internal and stdlib frames
strings String utilities: dedent, title-case, case conversion, whitespace handling
structs Struct field parsing and default-value application via tags
sync Synchronization primitives: Chan, Semaphore, WaitGroup, Once
uuid Deterministic RFC 4122 v8 UUIDs from strings using FNV-1a

About

Experimental packages

Resources

License

Stars

Watchers

Forks

Contributors

Languages