โ๏ธ samber/lo
vs samber/ro
Both samber/lo
and samber/ro
are powerful Go libraries, but they serve different purposes:
- samber/lo: A Lodash-like utility library for Go (bounded slices)
- samber/ro: A Reactive Programming library for Go (unbounded and event-driven streams)
This comparison will help you understand when to use each library and how they can complement each other.
Key Differencesโ
Paradigmโ
- lo: Synchronous functional programming
- ro: Asynchronous reactive programming
Data Flowโ
- lo: Immediate computation on finite collections
- ro: Stream processing on potentially infinite data sources
Use Casesโ
- lo: Data transformation, validation, filtering on existing data
- ro: Event handling, real-time processing, async workflows
The fundamental difference lies in how each library handles data flow and execution timing.
Code Comparisonโ
Data Transformationโ
samber/lo (synchronous):
package main
import (
"fmt"
"github.com/samber/lo"
)
func main() {
numbers := []int{1, 2, 3, 4, 5}
stage1 := lo.Filter(numbers, func(x int) bool {
return x%2==0
})
stage2 := lo.Map(stage1, func(x int, _ int) string {
return fmt.Sprintf("num-%d", x)
})
fmt.Println(stage2) // ["num-1", "num-2", "num-3", "num-4", "num-5"]
}
samber/ro:
package main
import (
"fmt"
"github.com/samber/ro"
)
func main() {
observable := ro.Pipe2(
ro.Just(1, 2, 3, 4, 5),
ro.Filter(func(x int) bool {
return x%2==0
}),
ro.Map(func(x int) string {
return fmt.Sprintf("num-%d", x)
}),
)
observable.Subscribe(ro.OnNext(func(s string) {
fmt.Println(s) // "num-2", "num-4"
}))
}
Notice how ro
processes values as a stream, while lo
processes the entire collection at once.
Filteringโ
samber/lo:
Results are available immediately after the function call.
numbers := []int{1, 2, 3, 4, 5}
evens := lo.Filter(numbers, func(x int, _ int) bool {
return x%2 == 0
})
// evens = [2, 4]
samber/ro:
observable := ro.Pipe(
ro.Just(1, 2, 3, 4, 5),
ro.Filter(func(x int) bool {
return x%2 == 0
}),
)
observable.Subscribe(ro.OnNext(func(x int) {
fmt.Println(x) // 2, 4
}))
:::
Filtering happens as values flow through the stream, providing lazy evaluation.
Async vs Syncโ
samber/lo (blocking):
All processing must complete before the function returns, blocking execution.
func processData(data []int) []string {
// Blocks until all processing is complete
return lo.Map(
lo.Filter(data, func() bool {
return i%2 == 1
}),
func(x int, _ int) string {
time.Sleep(100 * time.Millisecond) // blocking
return fmt.Sprintf("processed-%d", x)
},
)
}
func main() {
// Synchronous call
result := processData([]int{1, 2, 3})
fmt.Println(result) // appears after 200ms
}
samber/ro (non-blocking):
Values are processed as they arrive, without blocking the main execution flow.
var pipeline = ro.PipeOp3(
ro.Filter(func(x int) bool {
return x%2 == 1
})
ro.Map(func(x int) string {
return fmt.Sprintf("processed-%d", x)
}),
ro.DelayEach[string](100 * time.Millisecond)
)
func main() {
observable := pipeline(ro.Just(1, 2, 3))
// Non-blocking subscription
_ = observable.Subscribe(ro.OnNext(func(s string) {
fmt.Println(s) // appears immediately, one by one
}))
}
When to Use Whichโ
Use samber/lo when:โ
- Working with existing data collections
- Need immediate, synchronous results
- Performing data validation and transformation
- Writing utility functions and helpers
- Need comprehensive functional programming utilities
Use samber/ro when:โ
- Handling real-time or external events (clicks, websockets, timers)
- Working with infinite data sources
- Processing streaming data
- Building reactive user interfaces
- Implementing async workflows
- Need backpressure handling
Consider your specific use case requirements when choosing between these libraries.
Combining Both Librariesโ
You can use both libraries together for maximum flexibility:
Use lo
for data preparation and ro
for stream processing - they complement each other perfectly.
package main
import (
"fmt"
"github.com/samber/lo"
"github.com/samber/ro"
)
func main() {
// Use lo for initial data preparation
numbers := lo.Range(1, 11)
evens := lo.Filter(numbers, func(x int, _ int) bool {
return x%2 == 0
})
// Use ro for real-time processing
observable := ro.Pipe2(
ro.Just(evens...),
ro.Map(func(x int) string {
return fmt.Sprintf("stream-%d", x)
}),
)
observable.Subscribe(ro.OnNext(func(s string) {
fmt.Println(s)
}))
}
Performance Characteristicsโ
Aspect | samber/lo | samber/ro |
---|---|---|
Memory Usage | Higher (accumulate collections) | Lower (lazy producing) |
Latency | Low (blocks until complete) | medium (small overhead) |
CPU Usage | Predictable | Predictable |
Concurrency | None | Built-in |
Backpressure | Not applicable | Automatic |
Choose based on your specific performance requirements - lo
for immediate results, ro
for streaming efficiency.
Feature Comparisonโ
Feature | samber/lo | samber/ro |
---|---|---|
Map/Filter | โ | โ |
Reduce/Fold | โ | โ |
Async Processing | โ | โ |
Error Handling | Basic | Advanced |
Retry Mechanisms | โ | โ |
Time-based Operations | โ | โ |
Backpressure | โ | โ |
Hot/Cold Observables | โ | โ |
Subject Types | โ | โ |
Both libraries excel in their respective domains. Choose lo
for traditional functional programming on collections and ro
for reactive, event-driven applications.
- Explore samber/ro basics for reactive concepts
- See Operators guide for stream transformations
- Learn about backpressure in reactive systems