TL;DR: This package provides a small
Task[T]type withAwait, cancellation, panic-to-error conversion, and Promise-style combinators likeMap,Then,Catch,Finally,All,Any, andRace. Context-aware and avoids goroutine leaks.
Why: I like to create stuff without any particular reason.
go get github.com/unkn0wn-root/go-asyncctx := context.Background()
userTask := async.Start(ctx, func(ctx context.Context) (User, error) {
// expensive work...
return loadUser(ctx, 42)
})
// Await with cancellation from caller:
user, err := userTask.Await(ctx)
if err != nil { /* handle */ }
// Chain transformations:
nameTask := async.Map(ctx, userTask, func(ctx context.Context, u User) (string, error) {
return strings.ToUpper(u.Name), nil
})
name, _ := nameTask.Await(ctx)Channels are perfect for streaming many values or building long-lived pipelines. Task[T] is good when you're brave enough to adapt JS/C# like async patterns and/or want single-result computations with cancellation, panic-to-error conversion and so on. (I'm not responsible for any critics you'll get (and you will) from Gophers)
Without go-async
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
type userResult struct {
user User
err error
}
resultCh := make(chan userResult, 1)
go func() {
user, err := loadUser(ctx, 42)
resultCh <- userResult{user: user, err: err}
}()
var user User
var err error
select {
case out := <-resultCh:
user, err = out.user, out.err
case <-ctx.Done():
err = ctx.Err()
}With go-async
ctx := context.Background()
userTask := async.Start(ctx, func(ctx context.Context) (User, error) {
return loadUser(ctx, 42)
})
user, err := userTask.Await(ctx)Without go-async
resCh := make(chan struct {
name string
err error
}, 1)
go func() {
user, err := loadUser(ctx, 42)
if err != nil {
resCh <- struct {
name string
err error
}{"", err}
return
}
resCh <- struct {
name string
err error
}{strings.ToUpper(user.Name), nil}
}()
out := <-resCh
name, err := out.name, out.errWith go-async
userTask := async.Start(ctx, loadUser)
nameTask := async.Map(ctx, userTask, func(ctx context.Context, u User) (string, error) {
return strings.ToUpper(u.Name), nil
})
name, err := nameTask.Await(ctx)Without go-async
ctx, cancel := context.WithCancel(parent)
defer cancel()
var wg sync.WaitGroup
errs := make(chan error, len(endpoints))
values := make([]Data, len(endpoints))
for i, ep := range endpoints {
i, ep := i, ep
wg.Add(1)
go func() {
defer wg.Done()
val, err := fetch(ctx, ep)
if err != nil {
errs <- err
cancel()
return
}
values[i] = val
}()
}
go func() {
wg.Wait()
close(errs)
}()
for err := range errs {
if err != nil {
return nil, err
}
}
return values, nilWith go-async
tasks := make([]*async.Task[Data], len(endpoints))
for i, ep := range endpoints {
endpoint := ep
tasks[i] = async.Start(ctx, func(ctx context.Context) (Data, error) {
return fetch(ctx, endpoint)
})
}
values, err := async.AllCancelOnError(ctx, tasks...)- Less boilerplate for single-result async flows; no custom structs or select loops.
- Built-in composition helpers for mapping, chaining, and coordinating multiple results.
- Context-aware cancellation and panic-to-error conversion baked into every task.
- "Beloved" async/await pattern from JS/C# (I don't judge)
Map,Then,Catch,FinallyAll/AllCancelOnErrorAllSettledAny(first success)Race(first completion)DelayNewCompleter(externally resolve/reject a Task)
See the examples in examples_test.go.
- Context-propagation: every
Taskruns with its own derived context. - Cancellation:
Task.Cancel()cancels the task’s context,Await(ctx)also obeys the caller’s context (dual cancellation). - No leaks: all
Await-based combinators use the caller’s context to stop waiting; functions suffixed withCancelwill also explicitly callCancel()on remaining tasks to encourage prompt shutdown. - Panics become errors (
*async.PanicError) with stack trace. - Race-safety: all result writes happen-before
Doneis closed. - Zero dependencies: only standard library.
- This is not a replacement for channels at all and I don't try to replace anything. Use what you're most comfortable with.
- For best cancellation behavior in groups, create child tasks with the same
parent
ctxyou pass into combinators.