Awesome Go

go-future

CategoryGoroutines
SubcategoryGoroutines
Stars106

A Future/Promise library with generic combinators and a DAG execution engine

About go-future

This large README is shown as a plain-text preview. Read the full README at the source

# go-future

[![codecov](https://codecov.io/github/jizhuozhi/go-future/graph/badge.svg?token=9UZDVRZCQM)](https://codecov.io/github/jizhuozhi/go-future)
[![goreport](https://goreportcard.com/badge/github.com/jizhuozhi/go-future)](https://goreportcard.com/badge/github.com/jizhuozhi/go-future)

**go-future** is a lightweight, high-performance, mutex-free Future/Promise implementation for Go, built with modern concurrency in mind. It supports:

- Asynchronous task execution (`Async`, `CtxAsync`)
- Promise resolution (`Promise`)
- Event-driven callback registration (`Subscribe`)
- Functional chaining (`Then`, `ThenAsync`)
- Task composition (`AllOf`, `AnyOf`)
- Timeout control (`Timeout`, `Until`)
- Full support for Go generics
- **Fluent combinators as generic methods** (`Then[R]`, `Map[R]`, `FlatMap[R]`, `Cast[R]`, ...) — Go 1.27+

## 🔧 Requirements

**Go 1.18 or newer.** The module declares `go 1.18` and stays compatible with every release since.

The generic methods (`Then[R]`, `Map[R]`, `Cast[R]`, …) are an optional enhancement compiled only when the toolchain is **Go 1.27 or newer**, guarded by `//go:build go1.27`. Every transform that existed before v0.2.0 has both a method and a package-level form and keeps building on Go 1.18; `ThenGo`, `Map`, `FlatMap`, `Cast`, `Recover` and `OrElse` were added in v0.2.0 and exist in method form only, so those six need Go 1.27. See [Go 1.27 release notes](https://go.dev/doc/go1.27).

## 🔧 Installation

```bash
go get github.com/jizhuozhi/go-future
````

---

## ⬆️ Migrating to v0.2.0

v0.2.0 adds generic methods on top of the existing API. Exactly one group of symbols moved; everything else is additive and **no Go upgrade is required**.

### 1. Tuples moved to the `tuples` subpackage (breaking)

`Tuple2`…`Tuple16` and `Of2`…`Of16` are rarely used and added 30 exported
symbols to the main package, so they now live in their own package:

| v0.1.6                       | v0.2.0                                                       |
| ---------------------------- | ------------------------------------------------------------ |
| `future.Of2(f0, f1)`         | `tuples.Of2(f0, f1)`                                         |
| `future.Tuple2[A, B]`        | `tuples.Tuple2[A, B]`                                        |

```go
import "github.com/jizhuozhi/go-future/tuples"

f := tuples.Of2(fetchUser(), fetchAccount())
t, err := f.Get()
```

There is deliberately no alias left behind in `future`: `tuples` imports
`future`, so keeping one would create an import cycle.

### 2. No Go upgrade required

The module still declares `go 1.18`. The generic methods live in files guarded by `//go:build go1.27`, which tests the **toolchain** version rather than the `go` directive, so they light up automatically without touching `go.mod`.

| Toolchain      | What compiles                                             |
| -------------- | --------------------------------------------------------- |
| Go 1.18 – 1.26 | package-level functions only: `Then(f, cb)`, `Timeout(f, d)`, … |
| Go 1.27+       | both forms: `f.Then(cb)` **and** `Then(f, cb)`            |

"No Go upgrade required" is about code that already builds: every transform v0.1.6 exposed is still there on every toolchain. The six transforms introduced in v0.2.0 (`ThenGo`, `Map`, `FlatMap`, `Cast`, `Recover`, `OrElse`) are the exception — they are Go 1.27+ only, because they were written against generic methods and have no package-level form.

### 3. Package-level functions are not deprecated

They remain a fully supported API, not a compatibility shim. The two forms do **not** delegate to each other, by design: the package-level functions must not depend on a file that can be excluded from the build, so each variant carries its own (small, self-contained) body.

| Function           | Method form       |
| ------------------ | ----------------- |
| `Then(f, cb)`      | `f.Then(cb)`      |
| `ThenAsync(f, cb)` | `f.ThenAsync(cb)` |
| `ToAny(f)`         | `f.ToAny()`       |
| `ToChan(f)`        | `f.ToChan()`      |
| `Timeout(f, d)`    | `f.Timeout(d)`    |
| `Until(f, t)`      | `f.Until(t)`      |
| `Await(f)`         | `f.Get()`         |

`AllOf`, `AnyOf`, `Async`, `Done`, `NewPromise` and all `Promise` / `Future` methods are unchanged.

### New in v0.2.0 (Go 1.27+)

* Single-Future transforms as generic methods: `Then[R]`, `ThenAsync[R]`, `ThenGo[R]`, `Map[R]`, `FlatMap[R]`, `Cast[R]`, `Recover`, `OrElse`.
* `dagcore.NodeInstance.Cast[T]` and `dagfunc.Program.Value[T]` / `ValueAsync[T]` for reading `any`-typed results without assertions.
* `future.ErrTypeMismatch` for failed `Cast` conversions.

`dagfunc.Program.Get(sample any)` is **unchanged** — `Value[T]` is a type-safe alternative, not a replacement.

### New in v0.2.0: dagfunc reaches dagcore parity (additive)

`dagfunc` used to be a thin type-driven wrapper: one node per result type, one
accepted signature, no way to name a node or to wrap it. It now covers what
`dagcore` offers while keeping the type-based wiring:

* `Provide` and `Use` take options: `Name`, `After`, `From[T]`, `Timeout`,
  `Retry`, `Recover[R]`, `OrElse[R]`, `Wrap`, `Default`.
* `Use` accepts functions without a context, without an error, with several
  results, with no result at all, and functions returning a `*future.Future[R]`.
* `Subgraph` embeds a `Builder` as a node, `Group` namespaces nodes.
* `Compile` takes `dagcore` wrappers; `Program` exposes `Node`, `NodeByID` and
  `Instance`.

Everything that compiled before still compiles — `Provide(v)`, `Use(fn)`,
`Compile(inputs)`, `Run`, `Get(sample)` and `Value[T]` keep their signatures.
Two behaviours changed on purpose:

| Before                                                | Now                                                        |
| ----------------------------------------------------- | ---------------------------------------------------------- |
| `func() (int, error)` rejected with `ErrFuncSignature`  | valid: a source node with no context and no dependencies   |
| non-comparable output panicked when building `Run`'s map | skipped in the map, still readable with `Get` / `Value[T]` |

---

## 🚀 Quick Start

```go
package main

import (
	"fmt"
	"github.com/jizhuozhi/go-future"
)

func main() {
	p := future.NewPromise[string]()
	go func() {
		p.Set("hello", nil)
	}()
	val, err := p.Future().Get()
	fmt.Println(val, err) // Output: hello <nil>
}
```

---

## 🧠 Core Concepts

### Promise and Future

* `Promise` is the **producer**, which sets the value once.
* `Future` is the **consumer**, which retrieves the result asynchronously.

Every Future is backed by a single atomic word: each state transition is one compare-and-swap on that word, and waiters park on a semaphore. No mutex is taken on any path.

### Why a semaphore, not `sync.Cond`

`Get` parks a goroutine until the result is published. The textbook tool for that is a `sync.Cond`, but a `Cond` needs a `Locker`: waiting means unlocking, parking and locking again. `sync.Mutex` is adaptive — it spins, then backs off, then parks on a semaphore of its own — and that escalation pays for itself only when the critical section is short. An asynchronous task is a heavy operation, so the wait is long by nature and the mutex machinery would be overhead spent for nothing.

`go-future` therefore skips the mutex and drives the semaphore directly, keeping only the part a condition variable is actually needed for here: a queue of parked goroutines for `Set` to hand off to. There is no spin phase and no lock upgrade on the wait path.

The cost is that the semaphore is reached through `//go:linkname` rather than the public API. The Go source is candid about this — `runtime/sema.go` publishes both symbols with an explicit `//go:linkname` push and carries this note:

```text
sync_runtime_Semacquire should be an internal detail,
but widely used packages access it using linkname.
Notable members of the hall of shame include:
  - gvisor.dev/gvisor
  - github.com/sagernet/gvisor

Do not remove or change the type signature.
See go.dev/issue/67401.
```

The push makes this the handshake form rsc describes as the desired end state in [go.dev/issue/67401](https://go.dev/issue/67401), rather than an unauthorised pull, and "Do not remove or change the type signature" is a commitment the Go team has made. The note also records who else depends on it: gvisor.

---

## 🔨 Key APIs

### 🧭 Layering rule

One rule decides whether an operation is a package-level function or a method:

| Kind                       | Form        | Members                                                                                                          |
| -------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------- |
| Constructor                | function    | `Async`, `CtxAsync`, `Submit`, `CtxSubmit`, `Done`, `Done2`, `NewPromise`                                        |
| Single-Future transform    | **method**  | `Then`, `ThenAsync`, `ThenGo`, `Map`, `FlatMap`, `Cast`, `Recover`, `OrElse`, `ToAny`, `ToChan`, `Timeout`, `Until` |
| Combinator over N Futures  | function    | `AllOf`, `AnyOf`                                                                                                  |

A transform that changes the result type must introduce a type parameter of its own, so before Go 1.27 it had to be a package-level function taking the Future as its first argument. Since Go 1.27 it can be a method, and the method form is what you want to reach for: it chains left-to-right and it is discoverable through completion. v0.2.0 added six transforms (`ThenGo`, `Map`, `FlatMap`, `Cast`, `Recover`, `OrElse`) in method form only, so those six need Go 1.27.

Both forms coexist. The methods are compiled only under a Go 1.27+ toolchain (`//go:build go1.27`); the package-level functions that predate v0.2.0 are always available, which is what keeps Go 1.18 working.

A combinator over several Futures has no single receiver, and the language forbids a generic method from returning its receiver's type instantiated with a receiver-derived type (see the restrictions below) — so combinators stay functions. Java, Scala and friends draw the same line: "zip N futures into one" is a static/companion function there as well.

`AllOf` / `AnyOf` cover batches of Futures that share a type. Zipping Futures of
**unrelated** types needs one function per arity, so `Tuple2`…`Tuple16` and
`Of2`…`Of16` live in the [`tuples`](./tuples) subpackage — they are rarely
needed and would otherwise add 30 exported symbols to the main package:

```go
import "github.com/jizhuozhi/go-future/tuples"

f := tuples.Of2(fetchUser(), fetchAccount())
t, err := f.Get() // t.Val0 is a User, t.Val1 is an Account
```

---

### `Async(func() (T, error)) *Future[T]`

Starts a new asynchronous task in a goroutine.

```go
f := future.Async(func() (string, error) {
	return "result", nil
})
val, err := f.Get()
```

---

### `Promise[T]`

Used to create and control a future manually.

```go
p := future.NewPromise[int]()
go func() {
	p.Set(42, nil)
}()
val, _ := p.Future().Get()
```

---

### Single-Future transforms — `*Future[T]` methods

> **Requires a Go 1.27+ toolchain.** These files are guarded by `//go:build go1.27`. `ThenGo`, `Map`, `FlatMap`, `Cast`, `Recover` and `OrElse` are new in v0.2.0 and have no package-level form, so on Go 1.18–1.26 those six are unavailable; the other transforms have a counterpart in [Package-level transforms](#package-level-transforms-all-go-versions).

Go 1.27 lets a **method declare its own type parameters**, so transforms live on
`*Future[T]` and a pipeline reads left-to-right instead of inside-out.

```go
n, err := future.Async(fetchUser).
    Map(func(u User) string { return u.Email }).
    FlatMap(func(email string) *future.Future[Profile] { return future.Async(fetch(email)) }).
    Then(func(p Profile, err error) (int, error) {
        if err != nil {
            return 0, err
        }
        return len(p.Friends), nil
    }).
    Get()
```

| Method                                         | Description                                                                                                   |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `Then[R](func(T, error) (R, error))`            | Chains a synchronous step. Runs on the goroutine that completes the Future, so it must not block.              |
| `ThenAsync[R](func(T, error) *Future[R])`       | Chains an asynchronous step and flattens the nested Future.                                                    |
| `ThenGo[R](func(T, error) (R, error))`          | Like `Then`, but the callback is dispatched to the `Executor` so it may block. Panics are captured as `ErrPanic`. |
| `Map[R](func(T) R)`                             | Transforms the success value; on failure `fn` is skipped and the error is propagated.                          |
| `FlatMap[R](func(T) *Future[R])`                | `Map` + flatten; on failure `fn` is skipped and the error is propagated.                                       |
| `Cast[R]()`                                     | Reinterprets the Future as `*Future[R]` via a dynamic type assertion. Fails with `ErrTypeMismatch`.            |
| `Recover(func(error) (T, error))`               | Turns a failure into a success (or into another error).                                                        |
| `OrElse(T)`                                     | Replaces a failure with a default value.                                                                       |
| `ToAny()`, `ToChan()`, `Timeout(d)`, `Until(t)` | Interop and deadline helpers.                                                                                  |

`Cast` is what makes the `map[any]any` style results of `dagcore` / `dagfunc`
usable without type assertions:

```go
count, err := inst.Nodes()["func:TokenCount"].Cast[TokenCount]().Get()
```

#### ⚠️ Two language restrictions

1. **Interface methods may not declare type parameters, and a generic method
   cannot implement an interface method.** `Future` is therefore a concrete type
   by design; the transforms above are resolved statically and are deliberately
   not exposed through an interface.
2. **A generic method must not return its receiver's type instantiated with a
   type built from the receiver's type parameter.** The following looks natural
   but the compiler rejects it with `instantiation cycle`:

   ```go
   // func (f *Future[T]) Combine[R any](g *Future[R]) *Future[tuples.Tuple2[T, R]]
   ```

   Type-checking `Future[T]` would require `Future[Tuple2[T, R]]`, then
   `Future[Tuple2[Tuple2[T, R], R]]` and so on forever. Combining several
   Futures is exactly what the package-level combinators are for.

---

### Package-level transforms (all Go versions)

These are the original API and, on Go 1.18–1.26, the only form available. They are **not** deprecated, and they do not delegate to the methods above: each variant carries its own implementation so that the Go 1.18 build has no dependency on a file that may be excluded from it.

This list is deliberately shorter than the method list: the six transforms added in v0.2.0 (`ThenGo`, `Map`, `FlatMap`, `Cast`, `Recover`, `OrElse`) were written against generic methods and exist in method form only.

| Function           | Method form (Go 1.27+) |
| ------------------ | ---------------------- |
| `Then(f, cb)`      | `f.Then(cb)`           |
| `ThenAsync(f, cb)` | `f.ThenAsync(cb)`      |
| `ToAny(f)`         | `f.ToAny()`            |
| `ToChan(f)`        | `f.ToChan()`           |
| `Timeout(f, d)`    | `f.Timeout(d)`         |
| `Until(f, t)`      | `f.Until(t)`           |
| `Await(f)`         | `f.Get()`              |

---

### `AllOf(fs ...*Future[T]) *Future[[]T]`

Waits for all futures to complete successfully. Fails fast on the first error.

```go
f1 := future.Async(func() (int, error) { return 1, nil })
f2 := future.Async(func() (int, error) { return 2, nil })
fAll := future.AllOf(f1, f2)
vals, _ := fAll.Get() // [1, 2]
```

---

### `AnyOf(fs ...*Future[T]) *Future[AnyResult[T]]`

Returns the first successful result. If all fail, returns the first error.

Failure does not short-circuit: the all-failed outcome only becomes available once every input has failed, so one Future that never resolves keeps `AnyOf` pending even after the others have failed. A success settles the result immediately.

The Future `AnyOf` returns never fails itself: both outcomes arrive inside the `AnyResult`, so read `res.Err` rather than the Future's error. That follows from the return type — `*Future[AnyResult[T]]` has a field to carry the error, so failing the Future as well would say the same thing twice. `AllOf` returns `*Future[[]T]`, which has nowhere to put one, and reports failure through the Future instead.

```go
f1 := future.Async(func() (int, error) { return 0, fmt.Errorf("fail") })
f2 := future.Async(func() (int, error) { return 2, nil })

res, _ := future.AnyOf(f1, f2).Get()
// res.Index == 1, res.Val == 2, res.Err == nil

f3 := future.Async(func() (int, error) { return 0, fmt.Errorf("also fail") })
res, _ = future.AnyOf(f1, f3).Get()
// res.Index == 0, res.Err != nil — every input failed
```

---

### `Timeout(d time.Duration) *Future[T]` / `Until(t time.Time) *Future[T]`

Fails with `ErrTimeout` if the Future is not resolved in time.

Only the wrapper fails. The Future being wrapped keeps running and still resolves on its own schedule — nothing in this package cancels it, and there is no cancellation API to reach for. A timed-out Future tells the caller to stop waiting; it does not stop work. A task that should give up once its result is no longer wanted has to watch a `context.Context` itself (see `CtxAsync`), and it stays responsible for the resources it holds.

```go
f := future.Async(func() (int, error) {
	time.Sleep(2 * time.Second)
	return 42, nil
})
val, err := f.Timeout(time.Second).Get()
// err == future.ErrTimeout; f still completes about a second later
```

---

### `Done(val T) *Future[T]`, `Done2(val T, err error)`

Create a completed Future.

```go
f := future.Done("value")
f2 := future.Done2("value", nil)
```

---

### `Subscribe(cb func(T, error))`

Registers a callback that runs when the Future is done.

```go
f := future.Async(func() (int, error) { return 1, nil })
f.Subscribe(func(v int, err error) {
	fmt.Println("got:", v)
})
```

> ⚠️ Callbacks execute **in the same goroutine** that completes the Future. Avoid blocking operations in the callback.

---

## ✅ Advantages

* **Mutex-free:** Every state transition is a compare-and-swap on a single atomic word, and waiters park on a semaphore. No lock is taken on any path.
* **Type Safe:** Full support for Go generics.
* **No Goroutine Bloat:** Except `Async`, all operations are event-driven, avoiding extra goroutines.
* **Composable:** Easily chainable, supports DAG-like workflows.

---

## 📊 Benchmark

```text
goos: darwin
goarch: arm64
pkg: github.com/jizhuozhi/go-future
Benchmark/Promise           3.05M	    377 ns/op
Benchmark/WaitGroup         2.88M	    424 ns/op
Benchmark/Channel           3.00M	    399 ns/op
```

> `Promise` is competitive with `sync.WaitGroup` and `channel` in terms of performance and offers much better composition semantics.

---

# 📦 DAG Execution Engine

Starting from v0.1.4, `go-future` includes a **DAG (Directed Acyclic Graph) execution engine**, consisting of:

* `dagcore` — **stable**: a minimal parallel DAG scheduler with lock-free dependency tracking
* `dagfunc` — **experimental**: a high-level builder that constructs DAGs using Go function signatures with type-based dependency resolution

Frequently Asked Questions

What is go-future?

go-future is a Goroutines library for the Go programming language. A Future/Promise library with generic combinators and a DAG execution engine

How do I install go-future?

Install go-future with the Go module system using `go get jizhuozhi/go-future`. Check the repository for the current installation instructions.

What category does go-future belong to?

go-future is listed under Goroutines, specifically Goroutines.

← Back to Goroutines