Fiber — Patterns#

Concurrency patterns#

sync.Pool for zero-allocation recycling#

  • Usage: The single most prominent concurrency pattern. sync.Pool is used in 15+ places across core and client packages.
  • Example: app.go — a sync.Pool{New: NewDefaultCtx} recycles DefaultCtx instances on every HTTP request. binder/binder.go:19-43 pools all six binder types (header, cookie, query, form, resp-header). client/ pools Request, Response, CookieJar, and even the response/error channels for async execution.
  • Assessment: Extremely idiomatic and central to Fiber’s Express-in-Go performance claim. The pool pattern is applied consistently: acquire at entry, reset before release, defer the Put. The releasePooledBinder[T interface{ Reset() }] generic helper (bind.go:124) is an elegant abstraction that enforces the reset contract at the type level.

Background goroutine for periodic work#

  • Usage: 81 go func occurrences total. Non-test, non-client goroutines are intentionally narrow — 3-4 well-understood patterns.
  • Example:
    • middleware/logger/logger.go:36 — a single background goroutine refreshes the pre-formatted timestamp string every 500ms via atomic.Value store, avoiding a per-request time.Now() call.
    • middleware/cache/cache.go:158 — a ticker goroutine stores a uint64 Unix timestamp with atomic.StoreUint64 every 1s.
  • Assessment: These goroutines are fire-and-forget (they run for the lifetime of the process) and communicate only through atomic writes — no shared mutable state, no teardown needed. A simple and correct pattern for low-frequency background updates.

Goroutine + channel select for timeout racing#

  • Usage: middleware/timeout/timeout.go runs the user handler in a goroutine and races its result channel against the context deadline.
  • Example: timeout.go:44-80 — creates done chan error and panicChan chan any (both buffered to 1), launches go func() { done <- h(ctx) }(), then select { case err := <-done: ... case <-panicChan: ... case <-tCtx.Done(): ... }. On timeout, an “abandon” mechanism detaches the Ctx from the pool so the still-running goroutine can safely finish.
  • Assessment: A sophisticated pattern for handler timeouts — far beyond a naive context check. The use of buffered channels prevents goroutine leaks if the middleware has already returned. The Abandon/ForceRelease mechanism is a non-obvious but correct solution to the pool-ownership transfer problem.

Atomic operations for lock-free counters#

  • Usage: 89 total sync-primitive hits; atomic used for handler count in router.go and timestamp in logger/cache.
  • Example: router.go:573atomic.AddUint32(&app.handlersCount, uint32(len(handlers))) during route registration; middleware/logger/data.go:13Timestamp atomic.Value for lock-free timestamp reads.
  • Assessment: Appropriate use: small integers and value-replacement operations that don’t need a mutex. No complex CAS loops or custom lock-free data structures.

sync.Once for lazy one-time initialization#

  • Usage: 4 uses: mount.go (sub-app route building), middleware/static/static.go (filesystem initialization), middleware/logger/logger.go (error handler lookup).
  • Example: mount.go:24-26subAppsRoutesAdded sync.Once and subAppsProcessed sync.Once ensure that mounting sub-apps is safe to call from concurrent goroutines.
  • Assessment: Correct and minimal. Only used where initialization is genuinely expensive or racy.

Context cancellation + graceful shutdown#

  • Usage: listen.go:561gracefulShutdown goroutine listens on ListenConfig.GracefulContext and calls fasthttp.Server.ShutdownWithContext, then executes OnPreShutdown/OnPostShutdown lifecycle hooks, then shutdownServices.
  • Assessment: The shutdown flow is clean and testable. Services are stopped after the HTTP server drains, preventing handler code from hitting shuttered dependencies. The ShutdownTimeout config field (listen.go:106) bounds the shutdown duration.

Not used#

  • Worker pools: None — fasthttp manages its own concurrency pool at the HTTP layer.
  • Fan-out/fan-in: Not present in framework core; delegated to user handlers.
  • errgroup: Zero uses. The framework avoids structured concurrency — all goroutines are either fire-and-forget or short-racing (timeout middleware).

Error handling#

  • Style: Mixed — sentinel var Err... = errors.New(...) for framework-level conditions, a structured Error HTTP-status struct for HTTP responses, and a rich BindError struct for binding failures. Uses fmt.Errorf with %w for wrapping.
  • Error types defined:
    • app.go:62Error struct { Code int; Message string } — Fiber’s HTTP error. Has a constructor NewError(code, message) and implements error. Used by handlers to signal specific HTTP status codes.
    • bind.go:59BindError struct { Err error; Source string; Field string } — wraps a binding failure with metadata about where (URI, query, body, header, cookie) and what field failed. Implements Unwrap() for errors.As traversal.
    • error.go:12-20 — package-level sentinel errors: ErrGracefulTimeout, ErrNotRunning, ErrHandlerExited, errUnreachable.
    • client/core.go:297-303 — 7 sentinel client errors (URL format, schema, timeout, etc.).
    • extractors/extractors.go:64ErrNotFound for extractor misses.
    • Per-middleware sentinels: ErrMissingOrMalformedAPIKey (keyauth), ErrInvalidIdempotencyKey, ErrInvalidSHA256PasswordLength, etc.
  • Wrapping approach: fmt.Errorf("%w", err) used consistently in listen.go for system-level errors. BindError.Unwrap() chains back to the underlying decode error. No pkg/errors usage.
  • Examples:
    • listen.go:186fmt.Errorf("tls: cannot load TLS key pair from certFile=%q and keyFile=%q: %w", ...)
    • bind.go:98newBindError(BindSourceBody, err) creates a *BindError with source context.
    • binder/mapping.go:365errors.As(err, &convErr) inside extractFieldFromError to extract field names from schema errors.
  • Handler signature: func(Ctx) error — the framework’s single most important error-handling decision. All errors bubble to the global ErrorHandler, avoiding scattered http.Error() calls.

Configuration pattern#

  • Approach: Variadic config structNew(config ...Config) fiber.Handler is the universal signature for every middleware and factory in the project (30+ middleware packages, fiber.New, client.New, etc.).
  • Mechanics: Each package defines a Config struct and a package-level ConfigDefault variable. The New function copies ConfigDefault, applies the caller’s overrides, then fills in remaining zero-values field by field. Example from cors.go:28-41:
    cfg := ConfigDefault
    if len(config) > 0 {
        cfg = config[0]
        if len(cfg.AllowMethods) == 0 {
            cfg.AllowMethods = ConfigDefault.AllowMethods
        }
    }
  • Assessment: This is idiomatic “functional options lite.” It avoids the boilerplate of 20 With... functions while still allowing callers to pass only the fields they care about. The tradeoff is that zero-value detection is manual (you must compare to zero/nil, not “was this field explicitly set?”), but it works well in practice because config fields are rarely intentionally zeroed.
  • Not functional options: Fiber does not use the func(o *Options) functional-options pattern popularized by grpc-go. The config-struct approach is simpler for the user surface and easier to document.

Dependency injection#

  • Approach: Manual wiring — no Wire, Dig, or Fx.
  • Evidence:
    • Dependencies are passed to handlers via closures: app.Get("/users", handleUsers(db, cache)).
    • App.state (state.go) provides a typed key/value store for sharing data across handlers without globals. GetState[T](app.State(), "key") provides generic type-safe retrieval.
    • Config.Services []Service (services.go) is the lifecycle-management layer: services are started before request handling and stopped during graceful shutdown. This is structural DI — services declare their lifecycle, the framework orchestrates it.
  • Assessment: The Service interface is the most interesting DI-adjacent pattern in the codebase. It bridges “wiring” and “lifecycle” without requiring a container. For a library framework, manual DI is the correct call — imposing a DI framework would be over-reach.

Other notable patterns#

Code-generated interface (ifacemaker)#

The Ctx interface (ctx_interface_gen.go) and Res interface (res_interface_gen.go) are machine-generated from struct annotations by the ifacemaker tool. With 100+ methods on DefaultCtx, hand-maintaining the interface would be error-prone. This is an uncommon pattern in Go open-source projects — most projects accept interface/implementation drift or use smaller interfaces — and it solves a real maintenance problem. The generated file is checked in and regenerated in CI.

Interface composition in log package#

log/log.go uses fine-grained interface segregation and explicit embedding:

Logger         // 7 level methods (Trace..Panic)
FormatLogger   // 7 format methods (Tracef..Panicf)
WithLogger     // 7 structured methods (Tracew..Panicw)
CommonLogger   // embeds all three
ConfigurableLogger[T any]  // SetLevel, SetOutput, Logger() T  (generic)
AllLogger[T any]           // embeds CommonLogger + ConfigurableLogger[T]

This is textbook Interface Segregation Principle: consumers take CommonLogger or Logger depending on what they need. The generic ConfigurableLogger[T] and AllLogger[T] allow typed access to the underlying logger implementation without a type assertion.

Generics for type-safe state access#

state.go:101-130 and helpers.go:62 use Go generics for type-safe access to any-typed storage:

func GetState[T any](s *State, key string) (T, bool)
func MustGetState[T any](s *State, key string) T
func GetStateWithDefault[T any](s *State, key string, defaultVal T) T
func ValueFromContext[T any](ctx, key any) (T, bool)
func Convert[T any](value string, converter func(string) (T, error), ...) (T, error)

The generic pool helpers (binder/binder.go:81-91) use a constraint to enforce that pooled types are retrievable. This is well-targeted generics use — solving the type-assertion boilerplate problem without over-engineering.

Type switches for net/http adapter#

adapter.go (18+ type switch cases) bridges Fiber’s Handler type with net/http handler signatures. Four distinct http.Handler/http.HandlerFunc variant signatures are detected at runtime via switch h := handler.(type). This is a pragmatic compatibility shim — clean isolation of adapter logic, not scattered through the core.

Fluent builder in HTTP client#

client/request.go exposes a fluent method-chaining API:

client.New().Get(url).SetHeader("Authorization", "Bearer "+token).
    SetParam("page", "1").JSON(&result)

Every setter returns *Request, enabling a readable call chain. This mirrors the Express.js style of the server API and is appropriate for a client where a request is assembled in one place.

Table-driven tests#

294 instances of table-driven test helpers (testCases, tt.name, etc.) across *_test.go files. The framework itself is almost entirely tested this way. Tests use testify/require and testify/assert for assertions, stdlib testing.T.Run for sub-tests. The style is consistent and idiomatic across all 30+ middleware packages.

Dual dispatch path (fast path / custom path)#

router.go maintains two dispatch implementations: app.next() for the common *DefaultCtx case (avoids interface dispatch via type assertion) and app.nextCustom() for user-defined context types. This is a micro-optimization pattern — a deliberate fork in the hot path to maintain interface flexibility without paying the interface call overhead on the common case.

ConfigDefault + field-by-field override#

Every middleware follows an identical pattern: define ConfigDefault at package level, merge with caller’s config in New(), fill zero fields from the default. This “merge, then patch” pattern is easily understood by contributors and users alike, and it enables go doc to surface defaults directly in the struct comments.