Go stdlib extensions — admitted only when the standard library doesn't offer it.
The long-term ambition is the slot Guava fills in Java: the utility layer a project reaches for before hand-writing a helper. Go's ecosystem splits that slot today — samber/lo owns generic collection transforms, gods owns data structures, lancet goes kitchen-sink — and go-stdx competes on discipline, not breadth: stdlib-mirror naming, and only the small, common data operations every service would otherwise re-wrap.
The name is the admission rule: a hand-rolled max, a slices.Clone re-implementation, or a strconv wrapper does not belong here — use stdlib. What earns a slot is the three-to-five-line wrapper real projects keep re-writing because the stdlib deliberately omits it. Generating the underlying data (a UUID, a hash) is not our job — we lean on a mature library for that and wrap only the shape callers pass around.
Subpackages mirror stdlib naming so call sites read like the standard library they extend:
| package | what | why not stdlib / a lib |
|---|---|---|
slicesx |
Uniq, UniqBy — first-occurrence, order-preserving dedup |
the "seen map" loop everyone re-writes; stdlib's slices has no transform family |
stringsx |
Truncate / TruncateEllipsis — byte-budget cuts; FirstNonBlank; SplitAndTrim |
the log-truncation and human-written-list helpers every service re-writes (cmp.Or covers non-empty, not non-blank) |
osx |
EnvStr / EnvBool / EnvInt / EnvInt64 / EnvDuration — typed env lookups with defaults; WriteFileAtomic — temp+rename write |
config-from-env boilerplate in every service; os.WriteFile can leave readers a torn file |
ptrx |
To / Value / FormatOr — optional-field pointer helpers |
no stdlib answer to &literal (k8s.io/utils/ptr exists because of the gap) |
filepathx |
DirBytes — recursive regular-file byte count |
the quota/GC accounting walk everyone re-writes |
tarx |
PackDir / UnpackDir — directory ⇄ tar.gz with zip-slip defense |
archive/tar leaves both loops to the caller, and the extraction loop is famously easy to get wrong |
shellx |
Quote — POSIX single-quoting |
Go has no shlex; unquoted interpolation into a shell line is an injection |
netx |
IsDNSHostname — ASCII DNS hostname syntax without IP literals |
net parses IP addresses but does not export hostname validation |
randx |
Hex(n) — n random bytes as lowercase hex |
the "short random id" helper every daemon re-writes |
uuid |
New, NewWithPrefix, V4, V7, V7Hex — resource / random / time-ordered ids |
thin wrappers over google/uuid for the resource ID, string, and dashless-hex shapes services keep re-wrapping |
timeline |
operation/stage interfaces and detached, structured snapshots | joins stages from independent processes by business ID; snapshots retain intervals, hierarchy, optional actors and results |
Rules of the house:
- Don't reinvent the data, wrap the shape: no chasing zero dependencies — a mature foundational library (
google/uuid, …) is a fine dependency. What we add is the small wrapper around it, not a re-implementation of it. - stdlib-first: when Go's standard library grows an equivalent, the entry here is deprecated and removed.
- Positioning is the bar: anything genuinely generic that projects would otherwise hand-write belongs here — no waiting for N copies to accumulate first.
import (
"github.com/compforge/go-stdx/osx"
"github.com/compforge/go-stdx/randx"
"github.com/compforge/go-stdx/slicesx"
"github.com/compforge/go-stdx/uuid"
)
port := osx.EnvInt("APP_PORT", 8080)
id := "job-" + randx.Hex(6)
resourceID := uuid.NewWithPrefix("job")
ids := slicesx.Uniq(rawIDs)Used by case-code-review, hostel, and other Go projects under this account.
Record one operation across concurrent components and processes using its business ID. Each process constructs its own handle against the same Store. Stages retain their own intervals, parent IDs, results, and optional executor identity; parallel work stays parallel in the snapshot.
// Configure once per process with the application's existing *sql.DB.
store := sqlstore.New(db)
// Coordinator: New binds the ID; Start records the business start boundary.
tl, err := timeline.New(sandboxID, timeline.WithStore(store))
if err != nil {
return err
}
if err := tl.Start(ctx, "sandbox_start"); err != nil {
return err // recording error; the application chooses its policy
}
// Another process can construct the same handle without a remote Open call.
worker, err := timeline.New(sandboxID,
timeline.WithStore(store),
timeline.WithActor(timeline.Actor{Name: podName}), // optional
)
if err != nil {
return err
}
stage := worker.Begin("acquire_carrier")
operationErr := acquireCarrier(ctx)
stage.End(operationErr)
if err := worker.Flush(ctx); err != nil {
return err // retry Flush before publishing completion if completeness matters
}
// Or report an interval whose actual boundaries are already known.
if err := worker.Record(timeline.Stage{
ID: "pod-uid:image-pull:attempt-1", Name: "image_pull",
StartedAt: pullingAt, FinishedAt: pulledAt, Status: timeline.Succeeded,
}); err != nil {
return err
}
if err := worker.Flush(ctx); err != nil {
return err
}
// Coordinator records the business outcome; late stages can still be collected.
snapshot, captureErr := tl.Finish(ctx, operationErr)
// json.Marshal(snapshot) persists data directly; no separate application DTO.Import github.com/compforge/go-stdx/timeline and
github.com/compforge/go-stdx/timeline/sqlstore. Create the SQL schema through the
application's migrations before use; see storage and lifecycle.
The application owns connection pools, IO budgets and retention. SQLite is used
only by the storage integration tests; applications choose their database driver.
Without WithStore, New uses a private in-memory store. A shared
NewMemoryStore() joins handles in one process. The optional
timeline/gospan backend records into a local projection and seals its writer at
Finish; it does not aggregate across replicas. Its Registry remains process-local.
Pass timeline.Timeline directly to business functions. NewContext / FromContext
and BeginContext are optional helpers. StageFromContext / NewStageContext let shared recorders
carry a serializable parent reference across process boundaries.
Snapshot.Collection.LocalFlushed and StoreRead describe this handle's flush and
backend read. Neither claims all producers have reported. Actor is optional;
omitting it does not change recording. Document and Snapshot JSON deduplicate actors
into a payload-local actors table with 1-based stage actor_ref values; decoding
restores full Actor values. Standalone Stage JSON remains self-contained.
See the executable shared-store example and lifecycle, storage and collection contract.
MIT