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-dev skill (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-gopls skill (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/ro
Core 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
Pipe
- Subscription — 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 just OnNext. Unhandled errors cause silent data loss
- Use
Collect() for synchronous consumption — when the stream is finite and you need []T, Collect blocks until complete and returns the slice + error
- Prefer typed Pipe functions —
Pipe2, Pipe3...Pipe25 catch type mismatches at compile time. Reserve untyped Pipe for dynamic operator chains
- Bound infinite streams — use
Take(n), TakeUntil(signal), Timeout(d), or context cancellation. Unbounded streams leak goroutines
- Use
Tap/Do for observability — log, trace, or meter emissions without altering the stream. Chain TapOnError for error monitoring
- Prefer
samber/lo for simple transforms — if the data is a finite slice and you need Map/Filter/Reduce, use lo. Reach for ro when 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-lo skill for finite slice transforms (Map, Filter, Reduce, GroupBy) — use lo when data is already in a slice
- → See
samber/cc-skills-golang@golang-samber-mo skill for monadic types (Option, Result, Either) that compose with ro pipelines
- → See
samber/cc-skills-golang@golang-samber-hot skill for in-memory caching (also available as an ro plugin)
- → See
samber/cc-skills-golang@golang-concurrency skill for goroutine/channel patterns when reactive streams are overkill
- → See
samber/cc-skills-golang@golang-observability skill for monitoring reactive pipelines in production
1---
2name: golang-samber-ro
3description: "Reactive streams and event-driven programming in Golang using samber/ro — ReactiveX implementation with 150+ type-safe operators, cold/hot observables, 5 subject types (Publish, Behavior, Replay, Async, Unicast), declarative pipelines via Pipe, 40+ plugins (HTTP, cron, fsnotify, JSON, logging), automatic backpressure, error propagation, and Go context integration. Apply when using or adopting samber/ro, when the codebase imports github.com/samber/ro, or when building asynchronous event-driven pipelines, real-time data processing, streams, or reactive architectures in Go. Not for finite slice transforms (→ See `samber/cc-skills-golang@golang-samber-lo` skill)."
4user-invocable: true
5license: MIT
6compatibility: Designed for Claude Code, Codex or similar harness, and for projects using Golang.
7metadata:
8 author: samber
9 version: "1.2.2"
10 openclaw:
11 emoji: "👁"
12 homepage: https://github.com/samber/cc-skills-golang
13 requires:
14 bins:
15 - go
16 install: []
17 skill-library-version: "0.3.0"
18allowed-tools: Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*) Bash(git:*) Agent mcp__context7__resolve-library-id mcp__context7__query-docs AskUserQuestion Bash(godig:*) Bash(gopls:*) LSP mcp__gopls__*
19paths:
20 - "**/*.go"
21---
22
23**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.
24
25**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.
26
27# samber/ro — Reactive Streams for Go
28
29Go implementation of [ReactiveX](https://reactivex.io/). 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.
30
31**Official Resources:**
32
33- [github.com/samber/ro](https://github.com/samber/ro)
34- [ro.samber.dev](https://ro.samber.dev)
35- [pkg.go.dev/github.com/samber/ro](https://pkg.go.dev/github.com/samber/ro)
36
37This skill is not exhaustive — refer to library documentation and code examples for more information:
38
39- For Go package docs, symbols, versions, importers, and known vulnerabilities, → See `samber/cc-skills-golang@golang-pkg-go-dev` skill (`godig`), preferred over Context7 for Go package facts.
40- To navigate this library's usage in your own code (definitions, call sites, diagnostics), → See `samber/cc-skills-golang@golang-gopls` skill (`gopls`).
41- Context7 remains a fallback for docs not indexed on pkg.go.dev.
42
43## Why samber/ro (Streams vs Slices)
44
45Go 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.
46
47**When to use which tool:**
48
49| Scenario | Tool | Why |
50| --- | --- | --- |
51| Transform a slice (map, filter, reduce) | `samber/lo` | Finite, synchronous, eager — no stream overhead needed |
52| Simple goroutine fan-out with error handling | `errgroup` | Standard lib, lightweight, sufficient for bounded concurrency |
53| Infinite event stream (WebSocket, tickers, file watcher) | `samber/ro` | Declarative pipeline with backpressure, retry, timeout, combine |
54| Real-time data enrichment from multiple async sources | `samber/ro` | CombineLatest/Zip compose dependent streams without manual select |
55| Pub/sub with multiple consumers sharing one source | `samber/ro` | Hot observables (Share/Subjects) handle multicast natively |
56
57**Key differences: lo vs ro**
58
59| Aspect | `samber/lo` | `samber/ro` |
60| --- | --- | --- |
61| Data | Finite slices | Infinite streams |
62| Execution | Synchronous, blocking | Asynchronous, non-blocking |
63| Evaluation | Eager (allocates intermediate slices) | Lazy (processes items as they arrive) |
64| Timing | Immediate | Time-aware (delay, throttle, interval, timeout) |
65| Error model | Return `(T, error)` per call | Error channel propagates through pipeline |
66| Use case | Collection transforms | Event-driven, real-time, async pipelines |
67
68## Installation
69
70```bash
71go get github.com/samber/ro
72```
73
74## Core Concepts
75
76Four building blocks:
77
781. **Observable** — a data source that emits values over time. Cold by default: each subscriber triggers independent execution from scratch
792. **Observer** — a consumer with three callbacks: `onNext(T)`, `onError(error)`, `onComplete()`
803. **Operator** — a function that transforms an observable into another observable, chained via `Pipe`
814. **Subscription** — the connection between observable and observer. Call `.Wait()` to block or `.Unsubscribe()` to cancel
82
83```go
84observable := ro.Pipe2(
85 ro.RangeWithInterval(0, 5, 1*time.Second),
86 ro.Filter(func(x int) bool { return x%2 == 0 }),
87 ro.Map(func(x int) string { return fmt.Sprintf("even-%d", x) }),
88)
89
90observable.Subscribe(ro.NewObserver(
91 func(s string) { fmt.Println(s) }, // onNext
92 func(err error) { log.Println(err) }, // onError
93 func() { fmt.Println("Done!") }, // onComplete
94))
95// Output: "even-0", "even-2", "even-4", "Done!"
96
97// Or collect synchronously:
98values, err := ro.Collect(observable)
99```
100
101## Cold vs Hot Observables
102
103**Cold** (default): each `.Subscribe()` starts a new independent execution. Safe and predictable — use by default.
104
105**Hot**: multiple subscribers share a single execution. Use when the source is expensive (WebSocket, DB poll) or subscribers must see the same events.
106
107| Convert with | Behavior |
108| --- | --- |
109| `Share()` | Cold → hot with reference counting. Last unsubscribe tears down |
110| `ShareReplay(n)` | Same as Share + buffers last N values for late subscribers |
111| `Connectable()` | Cold → hot, but waits for explicit `.Connect()` call |
112| Subjects | Natively hot — call `.Send()`, `.Error()`, `.Complete()` directly |
113
114| Subject | Constructor | Replay behavior |
115| --- | --- | --- |
116| `PublishSubject` | `NewPublishSubject[T]()` | None — late subscribers miss past events |
117| `BehaviorSubject` | `NewBehaviorSubject[T](initial)` | Replays last value to new subscribers |
118| `ReplaySubject` | `NewReplaySubject[T](bufferSize)` | Replays last N values |
119| `AsyncSubject` | `NewAsyncSubject[T]()` | Emits only last value, only on complete |
120| `UnicastSubject` | `NewUnicastSubject[T](bufferSize)` | Single subscriber only |
121
122For subject details and hot observable patterns, see [Subjects Guide](./references/subjects-guide.md).
123
124## Operator Quick Reference
125
126| Category | Key operators | Purpose |
127| --- | --- | --- |
128| Creation | `Just`, `FromSlice`, `FromChannel`, `Range`, `Interval`, `Defer`, `Future` | Create observables from various sources |
129| Transform | `Map`, `MapErr`, `FlatMap`, `Scan`, `Reduce`, `GroupBy` | Transform or accumulate stream values |
130| Filter | `Filter`, `Take`, `TakeLast`, `Skip`, `Distinct`, `Find`, `First`, `Last` | Selectively emit values |
131| Combine | `Merge`, `Concat`, `Zip2`–`Zip6`, `CombineLatest2`–`CombineLatest5`, `Race` | Merge multiple observables |
132| Error | `Catch`, `OnErrorReturn`, `OnErrorResumeNextWith`, `Retry`, `RetryWithConfig` | Recover from errors |
133| Timing | `Delay`, `DelayEach`, `Timeout`, `ThrottleTime`, `SampleTime`, `BufferWithTime` | Control emission timing |
134| Side effect | `Tap`/`Do`, `TapOnNext`, `TapOnError`, `TapOnComplete` | Observe without altering stream |
135| Terminal | `Collect`, `ToSlice`, `ToChannel`, `ToMap` | Consume stream into Go types |
136
137Use typed `Pipe2`, `Pipe3` ... `Pipe25` for compile-time type safety across operator chains. The untyped `Pipe` uses `any` and loses type checking.
138
139For the complete operator catalog (150+ operators with signatures), see [Operators Guide](./references/operators-guide.md).
140
141## Common Mistakes
142
143| Mistake | Why it fails | Fix |
144| --- | --- | --- |
145| Using `ro.OnNext()` without error handler | Errors are silently dropped — bugs hide in production | Use `ro.NewObserver(onNext, onError, onComplete)` with all 3 callbacks |
146| Using untyped `Pipe()` instead of `Pipe2`/`Pipe3` | Loses compile-time type safety, errors surface at runtime | Use `Pipe2`, `Pipe3`...`Pipe25` for typed operator chains |
147| Forgetting `.Unsubscribe()` on infinite streams | Goroutine leak — the observable runs forever | Use `TakeUntil(signal)`, context cancellation, or explicit `Unsubscribe()` |
148| Using `Share()` when cold is sufficient | Unnecessary complexity, harder to reason about lifecycle | Use hot observables only when multiple consumers need the same stream |
149| 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 |
150| Not propagating context for cancellation | Streams ignore shutdown signals, causing resource leaks on termination | Chain `ContextWithTimeout` or `ThrowOnContextCancel` in the pipeline |
151
152## Best Practices
153
1541. **Always handle all three events** — use `NewObserver(onNext, onError, onComplete)`, not just `OnNext`. Unhandled errors cause silent data loss
1552. **Use `Collect()` for synchronous consumption** — when the stream is finite and you need `[]T`, `Collect` blocks until complete and returns the slice + error
1563. **Prefer typed Pipe functions** — `Pipe2`, `Pipe3`...`Pipe25` catch type mismatches at compile time. Reserve untyped `Pipe` for dynamic operator chains
1574. **Bound infinite streams** — use `Take(n)`, `TakeUntil(signal)`, `Timeout(d)`, or context cancellation. Unbounded streams leak goroutines
1585. **Use `Tap`/`Do` for observability** — log, trace, or meter emissions without altering the stream. Chain `TapOnError` for error monitoring
1596. **Prefer `samber/lo` for simple transforms** — if the data is a finite slice and you need Map/Filter/Reduce, use `lo`. Reach for `ro` when data arrives over time, from multiple sources, or needs retry/timeout/backpressure
160
161## Plugin Ecosystem
162
16340+ plugins extend ro with domain-specific operators:
164
165| Category | Plugins | Import path prefix |
166| --- | --- | --- |
167| Encoding | JSON, CSV, Base64, Gob | `plugins/encoding/...` |
168| Network | HTTP, I/O, FSNotify | `plugins/http`, `plugins/io`, `plugins/fsnotify` |
169| Scheduling | Cron, ICS | `plugins/cron`, `plugins/ics` |
170| Observability | Zap, Slog, Zerolog, Logrus, Sentry, Oops | `plugins/observability/...`, `plugins/samber/oops` |
171| Rate limiting | Native, Ulule | `plugins/ratelimit/...` |
172| Data | Bytes, Strings, Sort, Strconv, Regexp, Template | `plugins/bytes`, `plugins/strings`, etc. |
173| System | Process, Signal | `plugins/proc`, `plugins/signal` |
174
175For the full plugin catalog with import paths and usage examples, see [Plugin Ecosystem](./references/plugin-ecosystem.md).
176
177For real-world reactive patterns (retry+timeout, WebSocket fan-out, graceful shutdown, stream combination), see [Patterns](./references/patterns.md).
178
179If you encounter a bug or unexpected behavior in samber/ro, open an issue at [github.com/samber/ro/issues](https://github.com/samber/ro/issues).
180
181## Cross-References
182
183- → See `samber/cc-skills-golang@golang-samber-lo` skill for finite slice transforms (Map, Filter, Reduce, GroupBy) — use lo when data is already in a slice
184- → See `samber/cc-skills-golang@golang-samber-mo` skill for monadic types (Option, Result, Either) that compose with ro pipelines
185- → See `samber/cc-skills-golang@golang-samber-hot` skill for in-memory caching (also available as an ro plugin)
186- → See `samber/cc-skills-golang@golang-concurrency` skill for goroutine/channel patterns when reactive streams are overkill
187- → See `samber/cc-skills-golang@golang-observability` skill for monitoring reactive pipelines in production