https://github.com/jizhuozhi/go-future
Promise/Future in Go
https://github.com/jizhuozhi/go-future
future go promise sync
Last synced: 10 months ago
JSON representation
Promise/Future in Go
- Host: GitHub
- URL: https://github.com/jizhuozhi/go-future
- Owner: jizhuozhi
- License: apache-2.0
- Created: 2024-07-12T07:08:51.000Z (almost 2 years ago)
- Default Branch: master
- Last Pushed: 2024-11-04T03:04:56.000Z (over 1 year ago)
- Last Synced: 2024-11-04T04:17:44.906Z (over 1 year ago)
- Topics: future, go, promise, sync
- Language: Go
- Homepage:
- Size: 30.3 KB
- Stars: 35
- Watchers: 2
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# go-future
[](https://codecov.io/github/jizhuozhi/go-future)
[](https://goreportcard.com/badge/github.com/jizhuozhi/go-future)
**go-future** is a lightweight, high-performance, lock-free Future/Promise implementation for Go, built with modern concurrency in mind. It supports:
- Asynchronous task execution (`Async`, `CtxAsync`)
- ~~Lazy evaluation (`Lazy`)~~(Deprecated, will be removed in later version)
- 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
## 🔧 Installation
```bash
go get github.com/jizhuozhi/go-future
````
---
## 🚀 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
}
```
---
## 🧠 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 lock-free internal state. All state transitions are safe and efficient under high concurrency.
---
## 🔨 Key APIs
### `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()
```
---
### `Lazy(func() (T, error)) *Future[T]`
Returns a future that is lazily evaluated. The function is only executed once on the first call to `.Get()`.
```go
f := future.Lazy(func() (string, error) {
fmt.Println("evaluated")
return "lazy", nil
})
val, _ := f.Get() // prints "evaluated"
```
#### ⚠️ Deprecation Notice: Lazy
The Lazy API is deprecated and will be removed in future versions.
Although Lazy provides deferred execution semantics, it introduces implicit pull-based dependencies that are difficult to reason about in practice. Unlike Async, where execution is guaranteed upon construction, Lazy defers execution until .Get() is called — often far away from the site of definition.
This subtle semantic difference:
- Makes control flow harder to predict
- Breaks intuitive data dependency modeling
- Can result in unexpected bugs when chained or composed in concurrent settings
Recommendation: Prefer using Async or Promise for all use cases. These are explicit and deterministic.
---
### `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()
```
---
### `Then(f *Future[T], cb func(T, error) (R, error)) *Future[R]`
Chains computations synchronously.
```go
f := future.Async(func() (int, error) { return 1, nil })
f2 := future.Then(f, func(v int, err error) (string, error) {
return fmt.Sprintf("num:%d", v), err
})
result, _ := f2.Get()
```
---
### `ThenAsync(f *Future[T], cb func(T, error) *Future[R]) *Future[R]`
Chains computations with asynchronous return.
```go
f := future.Async(func() (int, error) { return 1, nil })
f2 := future.ThenAsync(f, func(v int, err error) *future.Future[string] {
return future.Async(func() (string, error) {
return fmt.Sprintf("async:%d", v), nil
})
})
result, _ := f2.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.
```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
```
---
### `Timeout(f *Future[T], d time.Duration) *Future[T]`
Wraps a future and fails with `ErrTimeout` if not resolved in time.
```go
f := future.Async(func() (int, error) {
time.Sleep(2 * time.Second)
return 42, nil
})
val, err := future.Timeout(f, time.Second).Get()
// err == future.ErrTimeout
```
---
### `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
* **Zero Locking:** Internals are implemented using atomic state machines, not `sync.Mutex`.
* **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 (Experimental)
Starting from v0.1.4, `go-future` introduces a powerful **DAG (Directed Acyclic Graph) execution engine**, consisting of:
* `dagcore`: A minimal, lock-free parallel DAG scheduler
* `dagfunc`: A high-level builder that constructs DAGs using Go function signatures with type-based dependency resolution
This enables users to describe complex data flow graphs declaratively with automatic dependency wiring and parallel execution.
## dagcore
`dagcore` is the low-level DAG execution engine powering [`go-future`](https://github.com/jizhuozhi/go-future)'s structured concurrency and dataflow execution model. It provides a lock-free, dependency-driven scheduler for executing static DAGs (Directed Acyclic Graphs) in parallel.
### ✨ Features
* ⚡ **Lock-free execution** via atomic dependency counters
* ⛓️ **Supports any static DAG with arbitrary fan-in/out structure**
* 🔁 **Exactly-once execution**: each node runs exactly once after its dependencies complete
* 🧠 **On-demand scheduling**: nodes are only triggered once all dependencies complete — goroutines are created only when the node is ready to run
* ❌ **Fast failure support**: optional early cancellation on error (fail-fast mode)
* ⏱️ **Context propagation**: full support for timeout and cancellation via `context.Context`
* 🧩 **Composable foundation**: designed for embedding in higher-level DAG builders (e.g. `dagfunc`)
* 📈 **Metrics & logging hooks**: supports per-node wrappers for observability (e.g. retry, timing, logging)
---
### 🚀 Example Usage
```go
dag := dagcore.NewDAG()
// Define DAG structure
_ = dag.AddInput("A")
_ = dag.AddNode("B", []dagcore.NodeID{"A"}, func(ctx context.Context, deps map[dagcore.NodeID]any) (any, error) {
return deps["A"].(int) + 2, nil
})
_ = dag.AddNode("C", []dagcore.NodeID{"A"}, func(ctx context.Context, deps map[dagcore.NodeID]any) (any, error) {
return deps["A"].(int) * 3, nil
})
// Verifies that the graph is complete and acyclic,
// then locks the structure to make it immutable for repeated safe instantiations.
if err := dag.Freeze(); err != nil {
return err
}
// Execute
inst, _ := dag.Instantiate(map[dagcore.NodeID]any{"A": 10})
res, _ := inst.Execute(context.Background())
fmt.Println("B:", res["B"], "C:", res["C"])
```
---
### 🧠 Execution Model
Each node in the DAG:
* Declares its dependencies via `AddNode(id, deps, func)`
* Executes only once **after all its inputs are ready**
* Will not allocate any goroutine until scheduled — **on-demand execution**
* May run in parallel with other ready nodes
* Propagates failures down dependent nodes (fail-fast)
Internally:
* Uses atomic counters to track pending dependencies per node
* Uses `future.Future` to propagate results, cancellation, and errors
* Can be fully composed and integrated with the rest of `go-future`
---
### ⚙️ API Overview
#### `dagcore.NewDAG() *DAG`
Creates a new empty DAG instance.
#### `(*DAG).AddInput(id NodeID) error`
Adds a node that must be externally provided during execution.
#### `(*DAG).AddNode(id NodeID, deps []NodeID, fn NodeFunc) error`
Adds a computational node with declared dependencies.
#### (*DAG).Freeze() error
**Freezes the DAG topology.** Verifies that the graph is complete and acyclic, then locks the structure to make it immutable for repeated safe instantiations.
```go
dag := dagcore.NewDAG()
// Add nodes...
_ = dag.Freeze()
```
> You must call Freeze() before Instantiate or Run. Once frozen, the DAG can be instantiated and executed multiple times in parallel.
#### `(*DAG).Instantiate(inputs map[NodeID]any, wrappers ...NodeFuncWrapper) (*DAGInstance, error)`
Creates a runtime instance of the DAG for execution.
#### `(*DAGInstance).Run(ctx context.Context) (map[NodeID]any, error)`
Executes all nodes and returns the final results.
#### `(*DAGInstance).RunAsync(ctx context.Context) *Future[map[NodeID]any]`
Runs asynchronously and returns a future.
---
### 🔧 Advanced Features
#### NodeFunc Wrapping
Use `NodeFuncWrapper` to wrap node logic for tracing, logging, retries, etc:
```go
dag.Instantiate(inputs, func(n *dagcore.NodeInstance, fn dagcore.NodeFunc) dagcore.NodeFunc {
return func(ctx context.Context, deps map[dagcore.NodeID]any) (any, error) {
start := time.Now()
out, err := fn(ctx, deps)
log.Printf("node %s took %s", n.ID(), time.Since(start))
return out, err
}
})
```
#### Mermaid Graph Output
Convert the DAG to a [Mermaid.js](https://mermaid-js.github.io/) compatible graph string:
```go
fmt.Println(dagcore.ToMermaid(instance))
```
---
### 🧱 Designed for Composition
`dagcore` is intended to be embedded in high-level tools:
* [`dagfunc`](../dagfunc): type-safe DAG builder with Go function signature inference
* Custom domain-specific orchestrators, AI pipelines, CI/CD workflows
* Any static dependency graph evaluation with result propagation
## dagfunc
`dagfunc` is a high-level, type-safe DAG (Directed Acyclic Graph) builder built on top of [`dagcore`](../dagcore). It allows you to define and execute dependency graphs by simply wiring Go functions based on their parameter and return types.
### 🚀 What It Solves
`dagfunc` abstracts away manual DAG construction by:
- Automatically inferring node dependencies from function signatures
- Resolving dependency order at compile-time
- Mapping types to results without needing manual wiring
Ideal for:
- AI agent pipelines
- Asynchronous service orchestration
- Task graph composition with clear dependency semantics
---
### ✅ Features
- ✅ Type-based dependency inference
- ✅ Fully integrated with `go-future` for parallel execution
- ✅ Reusable functions with Go-style declarations
- ✅ Built-in support for context propagation and error handling
- ✅ Alias support to distinguish same-type dependencies
---
### 🔧 Installation
```bash
go get github.com/jizhuozhi/go-future/dagfunc
````
---
### ✨ Example
```go
package main
import (
"context"
"fmt"
"github.com/jizhuozhi/go-future/dagfunc"
)
func main() {
type Input string
type TokenCount int
b := dagfunc.New()
// Step 1: Declare input
_ = b.Provide(Input(""))
// Step 2: Register function
_ = b.Use(func(ctx context.Context, text Input) (TokenCount, error) {
return TokenCount(len(text)), nil
})
// Step 3: Verifies that the graph is complete and acyclic,
// then locks the structure to make it immutable for repeated safe instantiations.
if err := b.Freeze(); err != nil {
panic(err)
}
// Step 4: Run
prog, _ := b.Compile([]any{Input("hello world")})
out, _ := prog.Run(context.Background())
fmt.Println(out[TokenCount(0)]) // Output: 11
}
```
---
### 🧠 Type-Based Wiring
`dagfunc` determines node dependencies using **parameter types** and result types:
* Each function must accept `context.Context` as the first argument
* Inputs and outputs must use unique Go types or **aliases**
* The DAG will automatically determine execution order
> ⚠️ If two inputs/outputs are of the same type (e.g., multiple `string` values), use `type alias` to disambiguate.
#### With type alias
```go
type UserID string
type Greeting string
b.Provide(UserID(""))
b.Use(func(ctx context.Context, uid UserID) (Greeting, error) {
return Greeting("Hello, " + string(uid)), nil
})
b.Use(func(ctx context.Context, g Greeting) (string, error) {
return string(g), nil
})
```
---
### 🧰 API Overview
#### `dagfunc.New() *Builder`
Creates a new DAG builder.
#### `(*Builder).Provide(val any) error`
Declares a root node with known value.
#### `(*Builder).Use(fn any) error`
Registers a function as a DAG node. Must match:
```go
func(ctx context.Context, A, B, ...) (X, Y, ..., error)
```
#### `(*Builder).Compile(inputs []any) (*Program, error)`
Builds a DAG using the provided inputs.
#### `(*Program).Run(ctx context.Context) (map[any]any, error)`
Executes the DAG. Outputs are keyed by result types with typed zero.
#### `(*Program).RunAsync(ctx context.Context) *future.Future[map[any]any]`
Executes the DAG. Return a future with outputs are keyed by result types with typed zero.
#### `(*Program).Get(any) (any, error)`
Gets the result value for a specific type.
#### Error propagation
* DAG execution will **fail fast** by default
* Downstream nodes will not be executed if inputs fail
* You can customize error behavior using `dagcore`
---
### 🧩 Relationship to dagcore
| Layer | Role |
| --------- | ----------------------------------------- |
| dagfunc | High-level: Build DAGs from Go functions |
| dagcore | Low-level: Execute DAGs with scheduling |
| go-future | Runtime: Power async execution via Future |
---
### 💡 Use Cases
* LLM / Agent planning pipelines
* Microservice DAG invocation
* Declarative orchestration of business logic
* Build systems / task runners
---
### 📌 Notes
* Outputs are retrieved by Go types (typed zero), not labels
* Type aliasing is required for disambiguation
* All dependencies must be resolvable at compile-time
## 🔐 License
Apache-2.0 license by [jizhuozhi](https://github.com/jizhuozhi)