Promise and Future Patterns in the aperturerobotics/util Go Library: Use Cases and Implementation
The aperturerobotics/util library implements type-safe Promise and Future patterns to handle asynchronous results with context cancellation, providing single-use promises, swappable containers, and memoized initialization for Go applications.
The aperturerobotics/util repository offers a lightweight, zero-dependency implementation of Promise and Future patterns designed specifically for Go’s concurrency model. Unlike generic future implementations, this library emphasizes context-aware cancellation, atomic result setting, and composability through a shared PromiseLike interface. The patterns enable goroutines to produce values asynchronously while allowing callers to await results with proper timeout and cancellation handling.
Core Components of the Promise/Future Abstraction
The library organizes its Promise and Future functionality into distinct types, each optimized for specific concurrency patterns. All components implement the PromiseLike[T] interface defined in promise/like.go, ensuring uniform access to the Await(context.Context) (T, error) method.
Promise[T] for Single-Use Asynchronous Results
The Promise[T] type in promise/promise.go represents a concrete, single-use asynchronous result. It maintains a done channel, the result value or error, and an atomic.Bool flag that guarantees the result is set at most once.
Use Promise[T] when you need:
- One-off async operations where the producer sets a value exactly once (network requests, background computations)
- Blocking behavior that waits until the result is ready or the context is cancelled
Key implementation details from promise/promise.go:
- Creation via
NewPromise[T]()(lines 20-22) - Result setting via
SetResult(value T, err error)with atomic guards (lines 46-53) - Awaiting via
Await(ctx)which checksctx.Done()before blocking (lines 56-63)
PromiseContainer[T] for Mutable Futures
The PromiseContainer[T] type in promise/container.go acts as a mutable holder that can swap the underlying Promise. Unlike the fixed Promise[T], containers allow long-running services to replace pending results as state changes occur.
Use PromiseContainer[T] when you need:
- Long-running services that must replace the pending result (routines that restart on state changes)
- Scenarios where multiple consumers "watch" a result that may be reproduced multiple times
The container uses the internal broadcast.Broadcast primitive to notify waiters when the underlying promise has been swapped (see SetPromise and GetPromise in container.go lines 29-33 and 50-56).
PromiseLike[T] Interface for Composability
All Promise and Future types implement PromiseLike[T], defined in promise/like.go. This interface requires only Await(context.Context) (T, error), allowing any component to treat promises, containers, and reference-counted futures uniformly.
Practical Use Cases for Promise and Future Patterns
The library demonstrates several architectural patterns where Promise and Future abstractions solve specific concurrency challenges in Go applications.
One-off Async Operations with Promise[T]
For simple asynchronous work that produces a single result, use the raw Promise[T] type. This pattern blocks callers until completion while respecting context cancellation.
package main
import (
"context"
"fmt"
"time"
"github.com/aperturerobotics/util/promise"
)
func asyncWork(ctx context.Context, p *promise.Promise[int]) {
// Simulate work.
time.Sleep(2 * time.Second)
// Resolve the promise (only succeeds once).
p.SetResult(42, nil)
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
p := promise.NewPromise[int]()
go asyncWork(ctx, p)
val, err := p.Await(ctx)
if err != nil {
fmt.Println("failed:", err)
return
}
fmt.Println("result:", val) // → 42
}
Key lines: creation (NewPromise) – promise/promise.go lines 20-22; setting result (SetResult) – promise/promise.go lines 46-53; awaiting (Await) – promise/promise.go lines 56-63.
Long-running Services with PromiseContainer[T]
When building services that restart or update their state, PromiseContainer[T] allows you to swap the future being awaited without recreating consumer goroutines.
package main
import (
"context"
"fmt"
"time"
"github.com/aperturerobotics/util/promise"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctr := promise.NewPromiseContainer[string]()
// Consumer: awaits the *current* promise.
go func() {
for {
p, wait := ctr.GetPromise()
if p == nil {
<-wait // wait for a promise to be set
continue
}
val, err := p.Await(ctx)
if err != nil {
fmt.Println("await error:", err)
return
}
fmt.Println("got:", val)
return
}
}()
// Producer: after a delay replace the promise with a resolved one.
time.Sleep(1 * time.Second)
ctr.SetResult("hello world", nil) // internally creates a new Promise
// Output: got: hello world
}
Key functions: NewPromiseContainer – promise/container.go lines 20-22; SetResult – promise/container.go lines 50-56; GetPromise – promise/container.go lines 29-33.
Memoized Initialization with Once[T]
The Once[T] type provides "run-once-with-retry" semantics. It memoizes successful results in a Promise and discards the promise on error, allowing subsequent calls to retry.
Use this for expensive initialization that should happen only once per successful run, such as establishing connections or loading caches, while still presenting a Future-like API.
package main
import (
"context"
"fmt"
"time"
"github.com/aperturerobotics/util/promise"
)
func main() {
ctx := context.Background()
once := promise.NewOnce(func(ctx context.Context) (int, error) {
fmt.Println("expensive init running")
time.Sleep(1 * time.Second)
return 123, nil // success
})
// First caller triggers the work.
val, _ := once.Resolve(ctx)
fmt.Println("first:", val)
// Second caller gets the memoized result instantly.
val, _ = once.Resolve(ctx)
fmt.Println("second:", val)
}
Key source: NewOnce – promise/once.go lines 20-22; the internal loop that retries on error – promise/once.go lines 64-71.
Publishing Routine Results via PromiseContainer
The routine.NewStateResultRoutine function builds a StateRoutine that automatically publishes its result into a PromiseContainer. This pattern turns any state-driven routine into a component that other system parts can await without tight coupling.
package main
import (
"context"
"fmt"
"time"
"github.com/aperturerobotics/util/promise"
"github.com/aperturerobotics/util/routine"
)
func producer(ctx context.Context, st int) (string, error) {
// Simulate work based on state.
time.Sleep(500 * time.Millisecond)
return fmt.Sprintf("state %d complete", st), nil
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Build a routine that automatically writes its result into a container.
stateR, resultCtr := routine.NewStateResultRoutine[int, string](producer)
// Run the routine in its own goroutine (the library’s StateRoutine manager would normally do this).
go func() {
_ = stateR(ctx, 7) // ignore the routine’s own error for demo
}()
// Await the result via the container (future‑style).
val, err := resultCtr.Await(ctx)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("routine result:", val) // → "state 7 complete"
}
Relevant source: NewStateResultRoutine – routine/result.go lines 17-22; result is stored with SetPromise and later SetResult (lines 30-38).
Reference-Counted Resources with AddRefPromise
The refcount.RefCount[T] type provides AddRefPromise, which returns a PromiseLike[T] together with a reference object. The promise fulfills when the ref-counted value resolves and clears when the last reference goes away. This supports resource-sharing patterns where callers need a future value but also deterministic release semantics.
package main
import (
"context"
"fmt"
"github.com/aperturerobotics/util/refcount"
)
func resolver(ctx context.Context, released func()) (string, func(), error) {
// Provide a value and a release hook.
return "shared‑value", func() { fmt.Println("resource released") }, nil
}
func main() {
ctx := context.Background()
rc := refcount.NewRefCount[string](ctx, false, nil, nil, resolver)
prom, ref := rc.AddRefPromise() // ← returns a PromiseLike
val, err := prom.Await(ctx)
if err != nil {
panic(err)
}
fmt.Println("got:", val) // → shared‑value
// When we are done:
ref.Release() // triggers the release hook when the last ref goes away
}
Key source: AddRefPromise – refcount/refcount.go lines 165-176; the PromiseContainer used to bridge the ref-counted value to a future.
Implementation Highlights from Source Code
The Promise and Future patterns in aperturerobotics/util distinguish themselves through several architectural decisions visible in the source code.
Context-Aware Cancellation
Every Await variant checks ctx.Done() before blocking, ensuring that awaiting goroutines abort promptly without leaking resources. This aligns with Go’s standard context propagation patterns.
Atomic Once-Only Semantics
In promise/promise.go, the Promise.isDone field is an atomic.Bool guaranteeing that SetResult cannot overwrite a completed promise (see lines 46-49). This prevents race conditions in concurrent environments.
Broadcast-Driven Replacement
PromiseContainer uses the internal broadcast.Broadcast primitive to notify waiters when the underlying promise has been swapped. This enables dynamic replacement of futures without recreating consumer logic (see SetPromise and GetPromise in promise/container.go lines 29-33 and 50-56).
Composable Higher-Level Utilities
The RefCount.AddRefPromise method and NewStateResultRoutine function demonstrate how the promise abstraction composes with reference counting and state-driven goroutine orchestration, respectively.
Summary
The Promise and Future patterns in aperturerobotics/util provide deterministic, composable asynchronous result handling for Go applications:
Promise[T]delivers single-use futures for one-off async operations with atomic result setting and context cancellation support.PromiseContainer[T]enables swappable futures for long-running services that restart or update their state dynamically.Once[T]provides memoized initialization with automatic retry semantics for expensive setup operations.PromiseLike[T]unifies all future types under a single interface for generic consumption.- Integration points like
routine.NewStateResultRoutineandrefcount.AddRefPromisedemonstrate real-world composition with goroutine management and resource lifecycle control.
Frequently Asked Questions
What is the difference between Promise[T] and PromiseContainer[T] in the util library?
Promise[T] is a single-use future that, once resolved with SetResult, cannot be changed or reused. It is ideal for one-off computations like network requests. PromiseContainer[T] acts as a mutable holder that can swap its underlying Promise via SetPromise or SetResult, making it suitable for long-running services that restart or update their asynchronous state dynamically.
How does the util library handle context cancellation with futures?
Every future implementation in the library, including Promise[T], PromiseContainer[T], and Once[T], implements the PromiseLike[T] interface which requires an Await(context.Context) method. Before blocking, these implementations check ctx.Done() to ensure goroutines can be aborted promptly without resource leaks, aligning with Go’s standard context propagation patterns.
When should I use Once[T] instead of a standard Promise[T]?
Use Once[T] when you need memoized initialization with automatic retry semantics. Unlike a raw Promise[T], which resolves once and stays resolved, Once[T] runs a callback lazily, stores the successful result in an internal Promise, and discards the promise on error. This allows subsequent calls to Resolve to retry the initialization, making it ideal for expensive setup operations like establishing database connections that might fail initially.
How does PromiseContainer[T] notify consumers when the underlying promise is swapped?
PromiseContainer[T] uses an internal broadcast.Broadcast primitive to signal waiters when the underlying promise changes. When SetPromise or SetResult is called, the container broadcasts the update, allowing consumers blocked on GetPromise to wake up and retrieve the new future. This mechanism enables dynamic replacement of futures without requiring consumers to poll or recreate their waiting logic.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →