Implement a Future/Promise type in Go with Get(ctx) and error propagation using generics.
Question 523MediumGo 1.22 to 1.25
The future is a struct holding the result plus a done channel that is closed exactly once when the result is ready. The Go memory model guarantees that a close happens-before any receive that returns because of it. So once <-f.done returns, reading val and err is race-free with no mutex. Any number of callers can call Get, and they all see the same result.
type Future[T any] struct {
done chan struct{}
val T
err error
}
func Async[T any](ctx context.Context, fn func(context.Context) (T, error)) *Future[T] {
f := &Future[T]{done: make(chan struct{})}
go func() {
defer close(f.done) // runs last (defers are LIFO)
defer func() {
if r := recover(); r != nil {
f.err = fmt.Errorf("future panicked: %v", r)
}
}()
f.val, f.err = fn(ctx)
}()
return f
}
func (f *Future[T]) Get(ctx context.Context) (T, error) {
select {
case <-f.done:
return f.val, f.err
case <-ctx.Done():
var zero T
return zero, ctx.Err()
}
}
// Then must be a function, not a method: Go methods cannot declare
// their own type parameters.
func Then[T, U any](ctx context.Context, f *Future[T], fn func(T) (U, error)) *Future[U] {
return Async(ctx, func(ctx context.Context) (U, error) {
v, err := f.Get(ctx)
if err != nil {
var zero U
return zero, err
}
return fn(v)
})
}
Gotchas:
Get(ctx)timing out does not stop the computation. Only thectxpassed toAsynccan do that, and only iffnhonours it.- Recovering the panic inside the goroutine matters. An unrecovered panic in any goroutine kills the whole process.
- The defer order is deliberate.
recoversetserrbeforeclosepublishes the result. func (f *Future[T]) Then[U any]does not compile: methods can't declare their own type parameters.
In real code, errgroup.Group or a plain buffered chan result is often enough. Interviewers want to see that you know the happens-before guarantee, not a JavaScript port.
More on Classic Concurrency Coding Problems
- Q521Implement a bounded blocking queue (producer-consumer) using sync.Cond, then using channels. Compare the two.
- Q522Implement a reusable barrier (CyclicBarrier) that N goroutines wait on before moving to the next phase.
- Q524Implement a concurrent prime sieve with a daisy chain of goroutines. How many goroutines does it create, and how do you stop it without leaks?
- Q525Concurrently crawl URLs (the Tour of Go web crawler) with depth limits, deduplication and bounded parallelism.
- Q526Count word frequencies in a large set of files concurrently and return the top K words. How do you merge per-worker maps efficiently?
- Q527Implement a debouncer and a throttler for a stream of events using time.Timer and channels.