Documentation
¶
Overview ¶
Package flow is an interface-driven library for concurrent pipelines: you implement the ends — Producer, Processor and Consumer (or pass a closure via the *Func adapters) — and flow owns the concurrency, ordering, back-pressure and fail-fast cancellation.
Inspired by Elixir's GenStage, but with push+blocking back-pressure and streaming reconstruction of the global input order.
Example (StructConsumer) ¶
Ends can be structs when they carry state; the consumer is called from a single goroutine, so no locking is needed.
package main
import (
"context"
"fmt"
"github.com/pabloos/flow"
)
// sum is a stateful Consumer implemented on a struct rather than a closure.
type sum struct{ total int }
func (s *sum) Consume(ctx context.Context, n int) error {
s.total += n
return nil
}
func main() {
total := &sum{}
err := flow.Run(context.Background(),
flow.Slice(1, 2, 3, 4),
flow.Map(func(n int) int { return n }),
total,
flow.Workers(4),
)
if err != nil {
fmt.Println("error:", err)
}
fmt.Println(total.total)
}
Output: 10
Index ¶
- func Run[I, O any](ctx context.Context, p Producer[I], proc Processor[I, O], c Consumer[O], ...) error
- func RunPartitioned[I, O any, K comparable](ctx context.Context, p Producer[I], proc Processor[I, O], c Consumer[O], ...) error
- type Consumer
- type ConsumerFunc
- type Option
- type Processor
- func Filter[T any](pred func(T) bool) Processor[T, T]
- func FlatMap[I, O any](fn func(I) []O) Processor[I, O]
- func Map[I, O any](fn func(I) O) Processor[I, O]
- func Then[A, B, C any](p1 Processor[A, B], p2 Processor[B, C]) Processor[A, C]
- func TryFilter[T any](pred func(T) (bool, error)) Processor[T, T]
- func TryFlatMap[I, O any](fn func(I) ([]O, error)) Processor[I, O]
- func TryMap[I, O any](fn func(I) (O, error)) Processor[I, O]
- type ProcessorFunc
- type Producer
- type ProducerFunc
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Run ¶
func Run[I, O any](ctx context.Context, p Producer[I], proc Processor[I, O], c Consumer[O], opts ...Option) error
Run wires producer -> pool of processors -> consumer and owns the concurrency, ordering, back-pressure and fail-fast cancellation. It returns the first error from any end, or nil.
Example ¶
The basic shape: a producer, a processor, a consumer, run in parallel with the output order preserved.
package main
import (
"context"
"fmt"
"github.com/pabloos/flow"
)
func main() {
err := flow.Run(context.Background(),
flow.Slice(1, 2, 3, 4),
flow.Map(func(n int) int { return n * 2 }),
flow.Each(func(n int) error {
fmt.Println(n)
return nil
}),
flow.Workers(4),
flow.Ordered(),
)
if err != nil {
fmt.Println("error:", err)
}
}
Output: 2 4 6 8
func RunPartitioned ¶ added in v0.2.0
func RunPartitioned[I, O any, K comparable](ctx context.Context, p Producer[I], proc Processor[I, O], c Consumer[O], key func(I) K, opts ...Option) error
RunPartitioned is like Run but routes every input to a worker by key: all values with the same key go to the same worker and are processed in order, so a given key is never processed concurrently. Use it for per-key ordering or for processors that keep per-key state.
Combine with Ordered to also restore the global input order at the consumer; on its own, RunPartitioned only guarantees order within each key.
Example ¶
RunPartitioned keeps each key's values on one worker, in order. With Ordered the global input order is restored too.
package main
import (
"context"
"fmt"
"github.com/pabloos/flow"
)
func main() {
type event struct {
user string
n int
}
events := []event{{"a", 1}, {"b", 1}, {"a", 2}, {"b", 2}}
err := flow.RunPartitioned(context.Background(),
flow.Slice(events...),
flow.Map(func(e event) string { return fmt.Sprintf("%s#%d", e.user, e.n) }),
flow.Each(func(s string) error {
fmt.Println(s)
return nil
}),
func(e event) string { return e.user },
flow.Workers(4),
flow.Ordered(),
)
if err != nil {
fmt.Println("error:", err)
}
}
Output: a#1 b#1 a#2 b#2
Types ¶
type Consumer ¶
Consumer receives each output value. It is never called concurrently, so implementations need no synchronization.
type ConsumerFunc ¶
type Option ¶
type Option func(*config)
Option configures a Run.
func Ordered ¶
func Ordered() Option
Ordered reconstructs the original input order at the consumer, even with a worker pool. Without it, outputs are consumed in arrival order.
type Processor ¶
Processor turns one input value into zero or more outputs via emit. Emitting once is Map, emitting conditionally is Filter, emitting many is FlatMap.
func Then ¶
Then composes two processors with no channel between them: it is plain function composition through emit, so multi-stage pipelines run in-process inside each worker.
Example ¶
A realistic multi-stage pipeline composed with Then: parse, filter, double.
package main
import (
"context"
"fmt"
"strconv"
"github.com/pabloos/flow"
)
func main() {
pipeline := flow.Then(
flow.TryMap(strconv.Atoi), // string -> int
flow.Then(
flow.Filter(func(n int) bool { return n > 0 }), // keep positives
flow.Map(func(n int) int { return n * 2 }), // double
),
)
var out []int
err := flow.Run(context.Background(),
flow.Slice("1", "-2", "3", "4"),
pipeline,
flow.Into(&out),
flow.Workers(4),
flow.Ordered(),
)
if err != nil {
fmt.Println("error:", err)
}
fmt.Println(out)
}
Output: [2 6 8]
func TryFlatMap ¶
TryFlatMap is FlatMap with a fallible expansion.
type ProcessorFunc ¶
Example ¶
One Processor can expand each input into several outputs (FlatMap) by emitting more than once.
package main
import (
"context"
"fmt"
"strings"
"github.com/pabloos/flow"
)
func main() {
split := flow.ProcessorFunc[string, string](func(ctx context.Context, line string, emit func(string) error) error {
for _, w := range strings.Fields(line) {
if err := emit(w); err != nil {
return err
}
}
return nil
})
var words []string
err := flow.Run(context.Background(),
flow.Slice("hello world", "goodbye"),
split,
flow.Into(&words),
flow.Ordered(),
)
if err != nil {
fmt.Println("error:", err)
}
fmt.Println(words)
}
Output: [hello world goodbye]