Golang Samber Ro
Samuel BertheCommunityCreated by samberDistributed by Samuel Berthe
- Skill
- Tool integration
- Software development
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
samber/ro is a Go implementation of ReactiveX that provides a type-safe, declarative approach to managing asynchronous data streams. It replaces complex, manual goroutine and channel wiring with a composable pipeline architecture. The library features over 150 operators, support for both cold and hot observables, automatic backpressure, and built-in error propagation. It is designed for scenarios involving infinite event streams, real-time data processing, and complex async coordination, offering a robust alternative to standard library concurrency primitives when declarative stream management is required.
What it does
- Provides 150+ type-safe operators for transforming, filtering, and combining data streams.
- Supports cold and hot observables for flexible event distribution and resource management.
- Includes 40+ plugins for common tasks like HTTP, cron, logging, and file system monitoring.
- Implements automatic backpressure and error propagation across pipeline stages.
- Integrates natively with Go context for cancellation and timeout management.
- Offers five subject types for advanced multicast and state-sharing patterns.
When to use it
- When building infinite event-driven pipelines (e.g., WebSockets, tickers, file watchers).
- When real-time data enrichment requires combining multiple asynchronous sources.
- When you need declarative retry, timeout, or fallback logic for remote service calls.
- When managing pub/sub patterns where multiple consumers share a single data source.
- When standard goroutine/channel patterns become too verbose or difficult to maintain.
Advantages
- Reduces boilerplate code compared to manual channel and goroutine management.
- Enhances code readability through declarative, chainable pipeline operators.
- Improves reliability with built-in error handling and context-aware cancellation.
- Provides compile-time type safety via typed Pipe functions.
- Simplifies complex async coordination like stream merging, zipping, and throttling.
Usage examples
- “Create a reactive pipeline that filters even numbers from an interval stream and maps them to strings.”
- The agent generates a Pipe2 chain using ro.RangeWithInterval, ro.Filter, and ro.Map, followed by a Subscribe call to handle the output.
- “How do I implement a remote API call with retries and a timeout using samber/ro?”
- The agent provides a pipeline using ro.Timeout and ro.RetryWithConfig to wrap the API call, ensuring robust execution.
- “Show me how to share a single WebSocket stream between two different consumers.”
- The agent demonstrates using ro.Share to convert a cold observable into a hot one, allowing multiple subscribers to receive the same events.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/samber/golang-samber-ro/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip golang-samber-ro.zip -d .agents/skills
Documentation by samber
Persona: You are a Go engineer who reaches for reactive streams when data flows asynchronously or infinitely. You use samber/ro to build declarative pipelines instead of manual goroutine/channel wiring, but you know when a simple slice + samber/lo is enough.
Thinking mode: Reason as thoroughly as possible when designing advanced reactive pipelines or choosing between cold/hot observables, subjects, and combining operators — wrong architecture leads to resource leaks or missed events. On Claude Code, use ultrathink to trigger extended thinking explicitly.
samber/ro — Reactive Streams for Go
Go implementation of ReactiveX. Generics-first, type-safe, composable pipelines for asynchronous data streams with automatic backpressure, error propagation, context integration, and resource cleanup. 150+ operators, 5 subject types, 40+ plugins.
Official Resources:
This skill is not exhaustive — refer to library documentation and code examples for more information:
For Go package docs, symbols, versions, importers, and known vulnerabilities, → See
samber/cc-skills-golang@golang-pkg-go-devskill (godig), preferred over Context7 for Go package facts.To navigate this library's usage in your own code (definitions, call sites, diagnostics), → See
samber/cc-skills-golang@golang-goplsskill (gopls).Context7 remains a fallback for docs not indexed on pkg.go.dev.
Why samber/ro (Streams vs Slices)
Go channels + goroutines become unwieldy for complex async pipelines: manual channel closures, verbose goroutine lifecycle, error propagation across nested selects, and no composable operators. samber/ro solves this with declarative, chainable stream operators.
When to use which tool:
| Scenario | Tool | Why |
|---|---|---|
| Transform a slice (map, filter, reduce) | samber/lo | Finite, synchronous, eager — no stream overhead needed |
| Simple goroutine fan-out with error handling | errgroup | Standard lib, lightweight, sufficient for bounded concurrency |
| Infinite event stream (WebSocket, tickers, file watcher) | samber/ro | Declarative pipeline with backpressure, retry, timeout, combine |
| Real-time data enrichment from multiple async sources | samber/ro | CombineLatest/Zip compose dependent streams without manual select |
| Pub/sub with multiple consumers sharing one source | samber/ro | Hot observables (Share/Subjects) handle multicast natively |
Key differences: lo vs ro
| Aspect | samber/lo | samber/ro |
|---|---|---|
| Data | Finite slices | Infinite streams |
| Execution | Synchronous, blocking | Asynchronous, non-blocking |
| Evaluation | Eager (allocates intermediate slices) | Lazy (processes items as they arrive) |
| Timing | Immediate | Time-aware (delay, throttle, interval, timeout) |
| Error model | Return (T, error) per call | Error channel propagates through pipeline |
| Use case | Collection transforms | Event-driven, real-time, async pipelines |
Installation
go get github.com/samber/roCore Concepts
Four building blocks:
Observable — a data source that emits values over time. Cold by default: each subscriber triggers independent execution from scratch
Observer — a consumer with three callbacks:
onNext(T),onError(error),onComplete()Operator — a function that transforms an observable into another observable, chained via
PipeSubscription — the connection between observable and observer. Call
.Wait()to block or.Unsubscribe()to cancel
observable := ro.Pipe2(
ro.RangeWithInterval(0, 5, 1*time.Second),
ro.Filter(func(x int) bool { return x%2 == 0 }),
ro.Map(func(x int) string { return fmt.Sprintf("even-%d", x) }),
)
observable.Subscribe(ro.NewObserver(
func(s string) { fmt.Println(s) }, // onNext
func(err error) { log.Println(err) }, // onError
func() { fmt.Println("Done!") }, // onComplete
))
// Output: "even-0", "even-2", "even-4", "Done!"
// Or collect synchronously:
values, err := ro.Collect(observable)Cold vs Hot Observables
Cold (default): each .Subscribe() starts a new independent execution. Safe and predictable — use by default.
Hot: multiple subscribers share a single execution. Use when the source is expensive (WebSocket, DB poll) or subscribers must see the same events.
| Convert with | Behavior |
|---|---|
Share() | Cold → hot with reference counting. Last unsubscribe tears down |
ShareReplay(n) | Same as Share + buffers last N values for late subscribers |
Connectable() | Cold → hot, but waits for explicit .Connect() call |
| Subjects | Natively hot — call .Send(), .Error(), .Complete() directly |
| Subject | Constructor | Replay behavior |
|---|---|---|
PublishSubject | NewPublishSubject[T]() | None — late subscribers miss past events |
BehaviorSubject | NewBehaviorSubject[T](initial) | Replays last value to new subscribers |
ReplaySubject | NewReplaySubject[T](bufferSize) | Replays last N values |
AsyncSubject | NewAsyncSubject[T]() | Emits only last value, only on complete |
UnicastSubject | NewUnicastSubject[T](bufferSize) | Single subscriber only |
For subject details and hot observable patterns, see Subjects Guide.
Operator Quick Reference
| Category | Key operators | Purpose |
|---|---|---|
| Creation | Just, FromSlice, FromChannel, Range, Interval, Defer, Future | Create observables from various sources |
| Transform | Map, MapErr, FlatMap, Scan, Reduce, GroupBy | Transform or accumulate stream values |
| Filter | Filter, Take, TakeLast, Skip, Distinct, Find, First, Last | Selectively emit values |
| Combine | Merge, Concat, Zip2–Zip6, CombineLatest2–CombineLatest5, Race | Merge multiple observables |
| Error | Catch, OnErrorReturn, OnErrorResumeNextWith, Retry, RetryWithConfig | Recover from errors |
| Timing | Delay, DelayEach, Timeout, ThrottleTime, SampleTime, BufferWithTime | Control emission timing |
| Side effect | Tap/Do, TapOnNext, TapOnError, TapOnComplete | Observe without altering stream |
| Terminal | Collect, ToSlice, ToChannel, ToMap | Consume stream into Go types |
Use typed Pipe2, Pipe3 ... Pipe25 for compile-time type safety across operator chains. The untyped Pipe uses any and loses type checking.
For the complete operator catalog (150+ operators with signatures), see Operators Guide.
Common Mistakes
| Mistake | Why it fails | Fix |
|---|---|---|
Using ro.OnNext() without error handler | Errors are silently dropped — bugs hide in production | Use ro.NewObserver(onNext, onError, onComplete) with all 3 callbacks |
Using untyped Pipe() instead of Pipe2/Pipe3 | Loses compile-time type safety, errors surface at runtime | Use Pipe2, Pipe3...Pipe25 for typed operator chains |
Forgetting .Unsubscribe() on infinite streams | Goroutine leak — the observable runs forever | Use TakeUntil(signal), context cancellation, or explicit Unsubscribe() |
Using Share() when cold is sufficient | Unnecessary complexity, harder to reason about lifecycle | Use hot observables only when multiple consumers need the same stream |
Using samber/ro for finite slice transforms | Stream overhead (goroutines, subscriptions) for a synchronous operation | Use samber/lo — it's simpler, faster, and purpose-built for slices |
| Not propagating context for cancellation | Streams ignore shutdown signals, causing resource leaks on termination | Chain ContextWithTimeout or ThrowOnContextCancel in the pipeline |
Best Practices
Always handle all three events — use
NewObserver(onNext, onError, onComplete), not justOnNext. Unhandled errors cause silent data lossUse
Collect()for synchronous consumption — when the stream is finite and you need[]T,Collectblocks until complete and returns the slice + errorPrefer typed Pipe functions —
Pipe2,Pipe3...Pipe25catch type mismatches at compile time. Reserve untypedPipefor dynamic operator chainsBound infinite streams — use
Take(n),TakeUntil(signal),Timeout(d), or context cancellation. Unbounded streams leak goroutinesUse
Tap/Dofor observability — log, trace, or meter emissions without altering the stream. ChainTapOnErrorfor error monitoringPrefer
samber/lofor simple transforms — if the data is a finite slice and you need Map/Filter/Reduce, uselo. Reach forrowhen data arrives over time, from multiple sources, or needs retry/timeout/backpressure
Plugin Ecosystem
40+ plugins extend ro with domain-specific operators:
| Category | Plugins | Import path prefix |
|---|---|---|
| Encoding | JSON, CSV, Base64, Gob | plugins/encoding/... |
| Network | HTTP, I/O, FSNotify | plugins/http, plugins/io, plugins/fsnotify |
| Scheduling | Cron, ICS | plugins/cron, plugins/ics |
| Observability | Zap, Slog, Zerolog, Logrus, Sentry, Oops | plugins/observability/..., plugins/samber/oops |
| Rate limiting | Native, Ulule | plugins/ratelimit/... |
| Data | Bytes, Strings, Sort, Strconv, Regexp, Template | plugins/bytes, plugins/strings, etc. |
| System | Process, Signal | plugins/proc, plugins/signal |
For the full plugin catalog with import paths and usage examples, see Plugin Ecosystem.
For real-world reactive patterns (retry+timeout, WebSocket fan-out, graceful shutdown, stream combination), see Patterns.
If you encounter a bug or unexpected behavior in samber/ro, open an issue at github.com/samber/ro/issues.
Cross-References
→ See
samber/cc-skills-golang@golang-samber-loskill for finite slice transforms (Map, Filter, Reduce, GroupBy) — use lo when data is already in a slice→ See
samber/cc-skills-golang@golang-samber-moskill for monadic types (Option, Result, Either) that compose with ro pipelines→ See
samber/cc-skills-golang@golang-samber-hotskill for in-memory caching (also available as an ro plugin)→ See
samber/cc-skills-golang@golang-concurrencyskill for goroutine/channel patterns when reactive streams are overkill→ See
samber/cc-skills-golang@golang-observabilityskill for monitoring reactive pipelines in production
Files
- ATTRIBUTION.mdAdded by AutoGineer905 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md11.8 KiB
- evals/evals.json17.3 KiB
- references/operators-guide.md14.6 KiB
- references/patterns.md8.0 KiB
- references/plugin-ecosystem.md6.3 KiB
- references/subjects-guide.md6.3 KiB
references/patterns.md
Reactive Patterns
Real-world patterns for building production reactive pipelines with samber/ro.
Table of Contents
Pattern 1: Remote Call with Retry and Timeout
Pattern 2: Continuous Event Stream (Hot Observable)
Pattern 3: Fan-In from Multiple Sources
Pattern 4: Dependent Data Combination
Pattern 5: Running Aggregation with Scan
Pattern 6: Error Recovery Cascade
Pattern 7: File System Watcher
Pattern 8: Graceful Shutdown
Pattern 9: Event-Driven Pipeline with Logging
Pattern 1: Remote Call with Retry and Timeout
Wrap a remote call (HTTP, gRPC, database) with automatic retry, exponential backoff, timeout, and fallback.
result := ro.Pipe3(
fetchUser(userID), // ro.Observable[User] — wraps your remote call
ro.Timeout[User](5*time.Second),
ro.RetryWithConfig[User](ro.RetryConfig{
Max: 3,
Delay: 500 * time.Millisecond,
BackoffMultiplier: 2.0,
MaxDelay: 5 * time.Second,
}),
ro.Catch[User](func(err error) ro.Observable[User] {
log.Printf("remote call failed after retries: %v, using cache", err)
return getCachedUser(userID)
}),
)
user, err := ro.Collect(result)Why ro over plain calls: declarative retry + timeout + fallback in 10 lines vs manual for-loops with sleep, context, and error tracking.
Pattern 2: Continuous Event Stream (Hot Observable)
Share a single long-lived connection (WebSocket, SSE, message queue) across multiple consumers.
// Cold observable wrapping any event stream source
eventStream := ro.NewObservable[TickerEvent](func(ctx context.Context, obs ro.Observer[TickerEvent]) error {
// connect to your stream source (WebSocket, NATS, Kafka, etc.)
for {
event, err := streamSource.Read(ctx)
if err != nil {
return err
}
obs.Next(event)
}
})
// Share: one connection, multiple consumers
shared := ro.Pipe1(eventStream, ro.Share[TickerEvent]())
// Consumer 1: update UI
shared.Subscribe(ro.OnNext(func(e TickerEvent) {
updateDashboard(e)
}))
// Consumer 2: record metrics
shared.Subscribe(ro.OnNext(func(e TickerEvent) {
metrics.RecordTick(e.Symbol, e.Price)
}))
// Consumer 3: alert on threshold
ro.Pipe1(shared, ro.Filter(func(e TickerEvent) bool {
return e.Price > alertThreshold
})).Subscribe(ro.OnNext(sendAlert))The rohttp plugin provides WebSocket and HTTP streaming observables (see Plugin Ecosystem).
Pattern 3: Fan-In from Multiple Sources
Merge events from multiple independent sources, batch, and process.
combined := ro.Pipe2(
ro.Merge(
apiStream,
pushStream,
cronScheduleStream,
),
ro.Distinct[Event](),
ro.BufferWithTimeOrCount[Event](100, 5*time.Second),
ro.Map(func(batch []Event) ProcessResult {
return processBatch(batch)
}),
)When to use Merge vs Concat vs Zip:
| Operator | Behavior | Use when |
|---|---|---|
Merge | Interleave: emit from any source as it arrives | Independent streams, order doesn't matter |
Concat | Sequential: finish first source, then start second | Ordered processing, fallback chains |
Zip | Pair: wait for one value from each source | Correlated data (user + settings, request + response) |
CombineLatest | Latest: re-emit combined whenever any source changes | Dependent state (price * quantity, config + data) |
Pattern 4: Dependent Data Combination
Combine data from multiple async sources that depend on each other.
// Fetch user and their orders in parallel, combine
profile := ro.Pipe1(
ro.CombineLatest2(
fetchUser(userID),
fetchOrders(userID),
),
ro.Map(func(pair lo.Tuple2[User, []Order]) UserProfile {
return UserProfile{
User: pair.A,
Orders: pair.B,
}
}),
)For independent data where you need exactly one value from each:
// Zip: waits for one value from each, pairs them
configAndData := ro.Zip2(loadConfig(), loadData())Pattern 5: Running Aggregation with Scan
Maintain running state across stream values — useful for dashboards, analytics, monitoring.
type Stats struct {
Count int
Sum float64
Avg float64
Max float64
}
statsStream := ro.Pipe2(
metricsStream,
ro.Scan(func(acc Stats, v float64) Stats {
acc.Count++
acc.Sum += v
acc.Avg = acc.Sum / float64(acc.Count)
if v > acc.Max {
acc.Max = v
}
return acc
}, Stats{}),
ro.SampleTime[Stats](5*time.Second), // emit stats every 5s
)Scan vs Reduce: Scan emits every intermediate state (good for live dashboards). Reduce emits only the final accumulated value (good for batch summaries).
Pattern 6: Error Recovery Cascade
Layer multiple error recovery strategies.
resilient := ro.Pipe3(
primaryDataSource,
// Strategy 1: retry transient failures
ro.RetryWithConfig[Data](ro.RetryConfig{
Max: 2,
Delay: time.Second,
}),
// Strategy 2: fall back to secondary source
ro.Catch[Data](func(err error) ro.Observable[Data] {
log.Warn("primary failed, trying secondary", "err", err)
return secondaryDataSource
}),
// Strategy 3: return cached/default value
ro.OnErrorReturn[Data](cachedDefault),
)Order matters: retry first (transient errors), then fallback source (persistent errors), then default value (total failure).
Pattern 7: File System Watcher
React to file changes with debouncing.
import rofsnotify "github.com/samber/ro/plugins/fsnotify"
watcher := ro.Pipe3(
rofsnotify.Watch("/etc/app/config/"),
ro.Filter(func(e fsnotify.Event) bool {
return e.Op&fsnotify.Write != 0
}),
ro.ThrottleTime[fsnotify.Event](2*time.Second), // debounce rapid saves
ro.Map(func(e fsnotify.Event) Config {
return reloadConfig(e.Name)
}),
)
watcher.Subscribe(ro.NewObserver(
func(cfg Config) { applyConfig(cfg) },
func(err error) { log.Error("config watch failed", "err", err) },
func() { log.Info("config watcher stopped") },
))Pattern 8: Graceful Shutdown
Use context or signal observable to cleanly terminate infinite streams.
import rosignal "github.com/samber/ro/plugins/signal"
// Method 1: OS signal
shutdown := rosignal.Notify(syscall.SIGTERM, syscall.SIGINT)
sub := ro.Pipe1(
workStream,
ro.TakeUntil[Work, os.Signal](shutdown),
).Subscribe(ro.NewObserver(
processWork,
handleError,
func() { log.Info("gracefully stopped") },
))
sub.Wait() // blocks until SIGTERM/SIGINT
// Method 2: Context cancellation
ctx, cancel := context.WithCancel(context.Background())
sub := ro.Pipe2(
workStream,
ro.ContextReset[Work](ctx),
ro.ThrowOnContextCancel[Work](),
).Subscribe(worker)
// Later: cancel() triggers clean shutdownPattern 9: Event-Driven Pipeline with Logging
Full production pipeline with observability at each stage.
import roslog "github.com/samber/ro/plugins/observability/slog"
pipeline := ro.Pipe5(
eventSource,
ro.TapOnSubscribe[Event](func() {
slog.Info("pipeline started")
}),
ro.Filter(func(e Event) bool { return e.Valid() }),
roslog.TapOnNext[Event](logger, slog.LevelDebug), // log each event
ro.Map(enrichEvent),
ro.BufferWithTimeOrCount[EnrichedEvent](50, 10*time.Second),
ro.MapErr(func(batch []EnrichedEvent) (Result, error) {
return persistBatch(batch)
}),
ro.TapOnError[Result](func(err error) {
slog.Error("pipeline error", "err", err)
metrics.IncrCounter("pipeline.errors", 1)
}),
ro.RetryWithConfig[Result](ro.RetryConfig{Max: 3, Delay: time.Second}),
)Requirements and compatibility
Written for: Claude Code, OpenAI Codex
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Declares allowed tools, Claude Code frontmatter
The author says: Designed for Claude Code, Codex or similar harness, and for projects using Golang.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- samber
- Publisher
- Samuel Berthe (Community)
- Distributed by
- Samuel Berthe
- Source
- samber/cc-skills-golang · skills/golang-samber-ro · commit bb1d43d
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by samber or Samuel Berthe.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download golang-samber-ro.zip