Fiber — Patterns#
Concurrency patterns#
sync.Pool for zero-allocation recycling#
- Usage: The single most prominent concurrency pattern.
sync.Poolis used in 15+ places across core and client packages. - Example:
app.go— async.Pool{New: NewDefaultCtx}recyclesDefaultCtxinstances on every HTTP request.binder/binder.go:19-43pools all six binder types (header, cookie, query, form, resp-header).client/poolsRequest,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. ThereleasePooledBinder[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 funcoccurrences 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 viaatomic.Valuestore, avoiding a per-requesttime.Now()call.middleware/cache/cache.go:158— a ticker goroutine stores auint64Unix timestamp withatomic.StoreUint64every 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.goruns the user handler in a goroutine and races its result channel against the context deadline. - Example:
timeout.go:44-80— createsdone chan errorandpanicChan chan any(both buffered to 1), launchesgo func() { done <- h(ctx) }(), thenselect { 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/ForceReleasemechanism 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;
atomicused for handler count inrouter.goand timestamp in logger/cache. - Example:
router.go:573—atomic.AddUint32(&app.handlersCount, uint32(len(handlers)))during route registration;middleware/logger/data.go:13—Timestamp atomic.Valuefor 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-26—subAppsRoutesAdded sync.OnceandsubAppsProcessed sync.Onceensure 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:561—gracefulShutdowngoroutine listens onListenConfig.GracefulContextand callsfasthttp.Server.ShutdownWithContext, then executesOnPreShutdown/OnPostShutdownlifecycle hooks, thenshutdownServices. - Assessment: The shutdown flow is clean and testable. Services are stopped after the HTTP server drains, preventing handler code from hitting shuttered dependencies. The
ShutdownTimeoutconfig 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 structuredErrorHTTP-status struct for HTTP responses, and a richBindErrorstruct for binding failures. Usesfmt.Errorfwith%wfor wrapping. - Error types defined:
app.go:62—Error struct { Code int; Message string }— Fiber’s HTTP error. Has a constructorNewError(code, message)and implementserror. Used by handlers to signal specific HTTP status codes.bind.go:59—BindError struct { Err error; Source string; Field string }— wraps a binding failure with metadata about where (URI, query, body, header, cookie) and what field failed. ImplementsUnwrap()forerrors.Astraversal.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:64—ErrNotFoundfor extractor misses.- Per-middleware sentinels:
ErrMissingOrMalformedAPIKey(keyauth),ErrInvalidIdempotencyKey,ErrInvalidSHA256PasswordLength, etc.
- Wrapping approach:
fmt.Errorf("%w", err)used consistently inlisten.gofor system-level errors.BindError.Unwrap()chains back to the underlying decode error. Nopkg/errorsusage. - Examples:
listen.go:186—fmt.Errorf("tls: cannot load TLS key pair from certFile=%q and keyFile=%q: %w", ...)bind.go:98—newBindError(BindSourceBody, err)creates a*BindErrorwith source context.binder/mapping.go:365—errors.As(err, &convErr)insideextractFieldFromErrorto 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 globalErrorHandler, avoiding scatteredhttp.Error()calls.
Configuration pattern#
- Approach: Variadic config struct —
New(config ...Config) fiber.Handleris the universal signature for every middleware and factory in the project (30+ middleware packages,fiber.New,client.New, etc.). - Mechanics: Each package defines a
Configstruct and a package-levelConfigDefaultvariable. TheNewfunction copiesConfigDefault, applies the caller’s overrides, then fills in remaining zero-values field by field. Example fromcors.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 bygrpc-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.
- Dependencies are passed to handlers via closures:
- Assessment: The
Serviceinterface 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.