Skip to content

Introduction

The Problem

Go makes concurrency easy to start but hard to get right. A bare go func() call has several pitfalls:

  • Unrecovered panics -- a panic in a goroutine crashes the entire process with no chance to log or handle it gracefully.
  • Lost errors -- errors returned inside goroutines are silently discarded unless you wire up channels or error groups yourself.
  • No observability -- you get no metrics, traces, or structured logs about goroutine lifecycle unless you instrument every call site manually.
  • No concurrency control -- it is easy to accidentally spawn thousands of goroutines with no backpressure.
  • Boilerplate -- every call site ends up reimplementing the same patterns: wait groups, error collection, context cancellation, panic recovery.

Why gofuncy?

Every production goroutine eventually needs panic recovery, error handling, and some form of observability. You can bolt these on one by one at each call site, or you can get them all from a single function call.

Safety by default. A bare go func() that panics takes down your entire process. gofuncy recovers every panic automatically, wraps it in a *PanicError with the full stack trace, and routes it through your error handling pipeline.

Observability without effort. Every goroutine gets an OpenTelemetry span, started/error/active counters, and optional duration histograms -- out of the box. No manual instrumentation at each call site.

Composable resilience. Retry, circuit breaker, fallback, and timeout are built-in options that compose in the correct order. You don't need to import three libraries and wire them together.

Performance overhead is negligible. With default telemetry (tracing + counters), gofuncy adds ~1-2μs per call. With telemetry disabled, overhead drops to ~120ns. For any goroutine that does real work (I/O, computation), this cost is invisible -- and it pays for itself the first time you debug a production issue using the traces and metrics you got for free.

What gofuncy Does

gofuncy provides a small set of functions that wrap common goroutine patterns with built-in safety, observability, and composability:

FunctionPurpose
GoFire-and-forget goroutine with panic recovery and error handling
StartLike Go, but blocks until the goroutine is running
StartWithReadyLike Go, but blocks until the goroutine signals readiness
StartWithStopLike Go, but the goroutine receives a stop function to cancel itself
GoWithCancelLike Go, but returns a stop function to shut down the goroutine
WaitSpawn a goroutine and collect the result later via a wait function
WaitWithStopLike Wait, but the goroutine receives a stop function to cancel itself
WaitWithReadyLike Wait, but blocks until the goroutine signals readiness
DoSynchronous execution with the full middleware chain (no goroutine)
NewGroupManaged set of concurrent functions with shared lifecycle
AllConcurrent iteration over a slice
MapConcurrent transformation of a slice, preserving order

The channel subpackage provides an observable generic channel wrapper with built-in metrics for queue depth, in-flight messages, and backpressure detection.

Every function supports a rich options pattern for configuring behavior per call.

Design Goals

Context-first -- every function takes a context.Context as its first parameter. Cancellation, timeouts, and value propagation work the way you expect.

Composable options -- configure behavior with functional options like WithName, WithTimeout, WithLimit, and WithMiddleware. Options are additive and can be shared or overridden per call.

Observable by default -- OpenTelemetry tracing and metrics are enabled out of the box. Every goroutine gets a span, started/error/active counters, and optional duration histograms. Disable what you don't need with WithoutTracing, WithoutStartedCounter, etc.

Panic-safe -- every goroutine spawned by gofuncy recovers from panics automatically. Panics are converted to *PanicError with the full stack trace preserved.

Minimal dependencies -- gofuncy depends only on the OpenTelemetry SDK and golang.org/x/sync for the weighted semaphore. No frameworks, no magic.