Skip to main content
Backend functions often depend on shared runtime services like loggers, database handles and other resources. Providers let you declare such dependencies with use and supply them at entry points using with. This way services don’t need to be threaded through every parameter list, and tests can swap in fakes or mocks without changing the function under test.

Two primitives

Parameters continue carrying highly dynamic data like request bodies. Providers carry services that typically rarely change (e.g. loggers, clocks and repositories). Keeping them separate clarifies what flows through your API boundary and what is ambient context.

Declare dependencies with use

Contracts are ordinary types: shapes with methods, or imported Go interfaces:
The compiler aggregates use sites into Providers(f) for each function and propagates requirements transitively through calls. You can see the obligation chain in diagnostics and LSP hovers: which services a handler needs, and which entry point must supply them.

Wire at the boundary

Entry points satisfy every Provider the call graph needs using a with { … } { … } block. Production wiring and test wiring use the same mechanism:
Wiring keys use the root contract name (Logger, Clock). Values must structurally satisfy the contract: plain structs with matching methods, or Go types that implement an imported interface.

Void contract methods

Contract methods without a return type (such as info(msg String)) must stay void on implementations.
  • Do not end a void method with a Go call that returns values, such as fmt.Println(...). With no declared return type, that trailing call becomes an implicit return, so the method no longer matches the void contract.
  • Prefer Forst println / print when you only need a side effect, or declare an explicit return type when you intend to return the call result.
See Return inference and implicit returns.

Shadowing

Nested with blocks merge with outer wiring. When the inner block supplies the same contract key, it shadows the outer value for that scope; other keys still forward from outside:

Tests

Tests reuse the same with blocks to swap fakes without a separate DI framework:

Compared to other approaches

We surveyed these patterns before settling on use and with. See the Providers RFC for the full prior art writeup. Forst treats context.Context separately from Providers. Declare services with use so requirements stay typed and visible. Use context for cancellation and request scoped values. Use with blocks for service wiring. Sidecar and TypeScript exports stay data only. The host wires Providers in Go before exposing a handler.

What gets emitted

Providers lower to a deduped struct parameter at the Go boundary. Production and test wiring compile to ordinary struct literals: readable Go, no runtime container.

Caveats

Providers are experimental. The RFC surface is still evolving. Pin your compiler version and check the Providers examples when upgrading.

Host wiring only

Providers lower in Go. They are not exported to TypeScript or sidecar invoke payloads. Functions with unsatisfied Providers(f) are omitted from generated clients. Wire with blocks in Go main before exposing handlers. See Call Forst over HTTP § Caveats and Generate client types § Caveats.

Void contract methods

  • A trailing Go multi-return call such as fmt.Println(...) becomes an implicit return and breaks a void contract method.
  • Prefer Forst println / print for side effects, or declare an explicit return type.
See Return inference and implicit returns.

context.Context is separate

Providers do not replace Go’s context.Context. They solve a different problem.
  • context.Context remains the right place for cancellation, deadlines, and request-scoped values such as a request ID or trace span. Keep passing ctx as a normal parameter when a function needs those signals.
  • Providers are for stable typed services declared with use: loggers, clocks, repositories, and similar host-supplied dependencies. The compiler tracks who must supply them at entry points.
  • Avoid stuffing those services into context.WithValue. That pattern is untyped, easy to miss at call sites, and has no compile-time obligation chain. Providers exist so you do not need to overload context for DI.
A handler can take both. Use context for the request lifecycle, and Providers for the services the host supplies once and reuses across calls.

Examples

See the provider examples on GitHub Deep dive: Providers RFC hub

Go interop

Import Go interfaces as Provider contracts.

Editor workflow

LSP hovers show Provider obligations and related diagnostics.