
Fluent collections for Go - with generics, chainable pipelines, and expressive data transforms. Inspired by Laravel, designed to feel natural in Go.
collection brings an expressive, fluent API to Go. Iterate, filter, transform, sort, reduce, group, and debug data with zero dependencies - familiar to Go developers, pleasant to use everywhere.
Features
- Fluent chaining - pipeline your operations like Laravel Collections
- Fully generic (
Collection[T]) - no reflection, nointerface{} - Minimal dependencies - small footprint (godump for debugging)
- Minimal allocations - slice views where possible; in-place ops reuse backing storage when semantics allow
- Map / Filter / Reduce - clean functional transforms
- First / Last / FirstWhere / IndexWhere / Contains helpers
- Sort, GroupBy, Chunk, and more
- Borrow-by-default - no defensive copies unless you ask for them
- Built-in JSON helpers (
ToJSON(),ToPrettyJSON()) - Developer-friendly debug helpers (
Dump(),Dd(),DumpStr()) - Works with any Go type, including structs, pointers, and deeply nested composites
Fluent Chaining
Many methods return the collection itself, allowing for fluent method chaining.
Some methods may be limited due to Go's generic constraints.
Fluent example:
examples/chaining/main.go
events := []DeviceEvent{
{Device: "router-1", Region: "us-east", Errors: 3},
{Device: "router-2", Region: "us-east", Errors: 15},
{Device: "router-3", Region: "us-west", Errors: 22},
}
// Fluent slice pipeline
collection.
New(events). // Construction
Shuffle(). // Ordering
Filter(func(e DeviceEvent) bool { return e.Errors > 5 }). // Slicing
Sort(func(a, b DeviceEvent) bool { return a.Errors > b.Errors }). // Ordering
Take(5). // Slicing
TakeUntilFn(func(e DeviceEvent) bool { return e.Errors < 10 }). // Slicing (stop when predicate becomes true)
SkipLast(1). // Slicing
Dump() // Debugging
// #[]main.DeviceEvent [
// 0 => #main.DeviceEvent {
// +Device => "router-3" #string
// +Region => "us-west" #string
// +Errors => 22 #int
// }
// ]Performance Benchmarks
tl;dr: lo is excellent. We solve a different problem - and in chained pipelines, that difference matters.
lo is a fantastic library and a major inspiration for this project. It is battle-tested, idiomatic, and often the right choice when you want small, standalone helpers that operate on slices in isolation.
collection takes a different approach.
Rather than treating each operation as an independent transformation, collection is built around explicit, fluent pipelines. Many operations are designed to mutate the same backing slice intentionally, allowing chained workflows to avoid intermediate allocations and unnecessary copying - while still making that behavior visible and documented.
That design choice doesn't matter much for some single operations. It matters a lot once you start chaining and especially in hot paths.
Below - A fixed ~24B allocation is the cost of the Collection wrapper (one-time per pipeline), not additional work per operation
The below tables are automatically generated from ./docs/bench/main.go.
Full raw tables: see BENCHMARKS.md.
Read-only scalar ops (wrapper overhead only)
| Op | Speed vs lo | Memory | Allocs |
|---|---|---|---|
| All | ≈ | +24B | +1 |
| Any | ≈ | +24B | +1 |
| None | ≈ | +24B | +1 |
| First | ≈ | +24B | +1 |
| Last | ≈ | +24B | +1 |
| FirstWhere | ≈ | +24B | +1 |
| IndexWhere | ≈ | +24B | +1 |
| Contains | ≈ | +24B | +1 |
| Reduce (sum) | ≈ | +24B | +1 |
| Sum | ≈ | +32B | +2 |
| Min | ≈ | +32B | +2 |
| Max | ≈ | +32B | +2 |
| Each | ≈ | +24B | +1 |
Transforming ops
| Op | Speed vs lo | Memory | Allocs |
|---|---|---|---|
| Chunk | 7.64x | -8.0KB | -49 |
| Take | ≈ | +48B | +2 |
| Skip | 71.40x | -8.2KB | ≈ |
| SkipLast | 71.90x | -8.2KB | ≈ |
| Zip | 2.35x | +48B | +2 |
| ZipWith | 3.15x | +48B | +2 |
| Unique | ≈ | +24B | +1 |
| UniqueBy | ≈ | +48B | +2 |
| Union | ≈ | +72B | +3 |
| Intersect | ≈ | +72B | +3 |
| Difference | 2.33x | -26.7KB | -29 |
| GroupBySlice | ≈ | +24B | +1 |
| CountBy | ≈ | +24B | +1 |
| CountByValue | ≈ | +24B | +1 |
| ToMap | ≈ | -24B | ≈ |
Pipelines
| Op | Speed vs lo | Memory | Allocs |
|---|---|---|---|
| Pipeline F→M→T→R | 1.79x | -12.2KB | ≈ |
Mutating ops
| Op | Speed vs lo | Memory | Allocs |
|---|---|---|---|
| Map | 2.21x | -8.2KB | ≈ |
| Filter | 1.41x | -8.2KB | ≈ |
| Reverse | ≈ | +24B | +1 |
| Shuffle | 1.57x | +24B | +1 |
How to read the benchmarks
- ≈ means the two libraries are effectively equivalent
- Explicit memory deltas show fixed wrapper overhead vs avoided allocations
- Single-operation helpers are intentionally close in performance if not exceeds
- Multi-step pipelines highlight the architectural difference
If you prefer immutable, one-off helpers - lo is outstanding. If you write expressive, chained data pipelines and care about hot-path performance - collection is built for that job.
Why chaining changes the performance story
Most functional helpers (including lo) operate like this:
input → Map → new slice → Filter → new slice → Take → new sliceThat model is simple and safe - but each step typically allocates.
collection pipelines are designed to look more like this:
input → Filter (in place) → Map (in place) → Take (slice view)When you opt into mutation, the pipeline stays on the same backing array unless an operation explicitly documents that it allocates. The result is:
- Fewer allocations
- Less GC pressure
- Lower end-to-end latency in hot paths
- Much stronger scaling in multi-step pipelines
That's why the biggest deltas appear in benchmarks like:
Pipeline F→M→T→RMapFilterChunkZip / ZipWithSkip / SkipLast
In these cases, collection can be 2×–30× faster and often reduce allocations to zero, not by doing "clever tricks", but by making mutation explicit and opt-in.
Explicit branching with Clone {#explicit-branching-with-clone}
Fluent pipelines don't mean you're locked into mutation.
This library borrows slices by default. It does not perform defensive copies. Use Clone() or ItemsCopy() to explicitly copy.
When you want to branch a pipeline or preserve the original data, Clone() creates a shallow copy of the collection so subsequent operations are isolated and predictable.
events := collection.New(deviceEvents)
// Fast alerting path: cheap filters, early exit
alerts := events.
Clone().
Filter(func(e DeviceEvent) bool { return e.Severity >= Critical }).
Take(10)
// Deeper analysis path: heavier work, full ordering
report := events.
Clone().
Filter(func(e DeviceEvent) bool { return e.Region == "us-east" }).
Sort(func(a, b DeviceEvent) bool { return a.Timestamp.Before(b.Timestamp) })This keeps the performance benefits of in-place operations where they matter, while making divergence points explicit and intentional.
No hidden copies. No surprises.
Design Principles
- Type-safe: no reflection
- Explicit semantics: order, mutation, and allocation are documented
- Go-native: respects generics and stdlib patterns
- Eager evaluation: no lazy pipelines or hidden concurrency
- Maps are boundaries: unordered data is handled explicitly
What this library is not
- Not a lazy or streaming library
- Not concurrency-aware
- Not immutable-by-default
- Not a replacement for idiomatic loops in simple cases
- Not designed to hide allocation, mutation, or ordering semantics
Working with maps
Maps are unordered in Go. This library does not pretend otherwise.
Instead, map interaction is explicit and intentional:
FromMapmaterializes key/value pairs into an ordered workflowToMapreduces collections back into maps explicitlyToMapKVprovides a convenience forPair[K,V]
This makes transitions between unordered and ordered data visible and honest.
Behavior semantics
Each method declares how it interacts with the collection:
- readonly - reads data only and returns a derived value
- immutable - returns a new collection; the original is unchanged
- mutable - modifies the collection in place and returns the same instance
- terminal - ends the fluent pipeline and returns a non-collection result
These annotations describe observable behavior, not implementation details.
Terminal operations do not return a Collection and cannot be chained further. They are designed to be allocation-free under New() where possible.
Allocation and copying are explicitly documented per method. Some readonly or immutable operations may allocate internally when required (e.g. grouping, chunking, scratch copies), but never mutate the receiver.
Borrowed slices, in-place mutation, and view semantics are intentional and visible.
Runnable examples
Every function has a corresponding runnable example under ./examples.
These examples are generated directly from the documentation blocks of each function, ensuring the docs and code never drift. These are the same examples you see here in the README and GoDoc.
An automated test executes every example to verify it builds and runs successfully.
This guarantees all examples are valid, up-to-date, and remain functional as the API evolves.
Installation
go get github.com/goforj/collectionAPI Index
| Group | Functions |
|---|---|
| Access | Items ItemsCopy |
| Aggregation | Avg Count CountBy CountByValue Max MaxBy Median Min MinBy Mode Reduce Sum |
| Construction | Clone New NewNumeric |
| Debugging | Dd Dump DumpStr |
| Grouping | GroupBy GroupBySlice |
| Maps | FromMap ToMap ToMapKV |
| Ordering | After Before Reverse Shuffle Sort |
| Querying | All Any At Contains First FirstWhere IndexWhere IsEmpty Last LastWhere None |
| Serialization | ToJSON ToPrettyJSON |
| Set Operations | Difference Intersect SymmetricDifference Union Unique UniqueBy UniqueComparable |
| Slicing | Chunk Filter Partition Pop PopN Skip SkipLast Take TakeLast TakeUntil TakeUntilFn Window |
| Transformation | Append Concat Each Map MapTo Merge Multiply Pipe Prepend Tap Times Transform Zip ZipWith |
Access
Items · readonly · terminal
Items returns the backing slice of items.
Example: integers
c := collection.New([]int{1, 2, 3})
items := c.Items()
collection.Dump(items)
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// ]Example: strings
c2 := collection.New([]string{"apple", "banana"})
items2 := c2.Items()
collection.Dump(items2)
// #[]string [
// 0 => "apple" #string
// 1 => "banana" #string
// ]Example: structs
type User struct {
ID int
Name string
}
users := collection.New([]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
})
out := users.Items()
collection.Dump(out)
// #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// +Name => "Alice" #string
// }
// 1 => #main.User {
// +ID => 2 #int
// +Name => "Bob" #string
// }
// ]ItemsCopy · readonly · terminal
ItemsCopy returns a copy of the collection's items.
c := collection.New([]int{1, 2, 3})
items := c.ItemsCopy()
collection.Dump(items)
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// ]Aggregation
Avg · readonly · terminal
Avg returns the average of the collection values as a float64. If the collection is empty, Avg returns 0.
Example: integers
c := collection.NewNumeric([]int{2, 4, 6})
collection.Dump(c.Avg())
// 4.000000 #float64Example: float
c2 := collection.NewNumeric([]float64{1.5, 2.5, 3.0})
collection.Dump(c2.Avg())
// 2.333333 #float64Count · readonly · terminal
Count returns the total number of items in the collection.
count := collection.New([]int{1, 2, 3, 4}).Count()
collection.Dump(count)
// 4 #intCountBy · readonly · terminal
CountBy returns a map of keys extracted by fn to their occurrence counts. K must be comparable.
Example: integers
c := collection.New([]int{1, 2, 2, 3, 3, 3})
counts := collection.CountBy(c, func(v int) int {
return v
})
collection.Dump(counts)
// #map[int]int {
// 1 => 1 #int
// 2 => 2 #int
// 3 => 3 #int
// }Example: strings
c2 := collection.New([]string{"apple", "banana", "apple", "cherry", "banana"})
counts2 := collection.CountBy(c2, func(v string) string {
return v
})
collection.Dump(counts2)
// #map[string]int {
// apple => 2 #int
// banana => 2 #int
// cherry => 1 #int
// }Example: structs
type User struct {
Name string
Role string
}
users := collection.New([]User{
{Name: "Alice", Role: "admin"},
{Name: "Bob", Role: "user"},
{Name: "Carol", Role: "admin"},
{Name: "Dave", Role: "user"},
{Name: "Eve", Role: "admin"},
})
roleCounts := collection.CountBy(users, func(u User) string {
return u.Role
})
collection.Dump(roleCounts)
// #map[string]int {
// admin => 3 #int
// user => 2 #int
// }CountByValue · readonly · terminal
CountByValue returns a map where each distinct item in the collection is mapped to the number of times it appears.
Example: strings
c1 := collection.New([]string{"a", "b", "a"})
counts1 := collection.CountByValue(c1)
collection.Dump(counts1)
// #map[string]int {
// a => 2 #int
// b => 1 #int
// }Example: integers
c2 := collection.New([]int{1, 2, 2, 3, 3, 3})
counts2 := collection.CountByValue(c2)
collection.Dump(counts2)
// #map[int]int {
// 1 => 1 #int
// 2 => 2 #int
// 3 => 3 #int
// }Example: structs (comparable)
type Point struct {
X int
Y int
}
c3 := collection.New([]Point{
{X: 1, Y: 1},
{X: 2, Y: 2},
{X: 1, Y: 1},
})
counts3 := collection.CountByValue(c3)
collection.Dump(counts3)
// #map[main.Point]int {
// {1 1} => 2 #int
// {2 2} => 1 #int
// }Max · readonly · terminal
Max returns the largest numeric item in the collection. The second return value is false if the collection is empty.
Example: integers
c := collection.NewNumeric([]int{3, 1, 2})
max1, ok1 := c.Max()
collection.Dump(max1, ok1)
// 3 #int
// true #boolExample: floats
c2 := collection.NewNumeric([]float64{1.5, 9.2, 4.4})
max2, ok2 := c2.Max()
collection.Dump(max2, ok2)
// 9.200000 #float64
// true #boolExample: empty numeric collection
c3 := collection.NewNumeric([]int{})
max3, ok3 := c3.Max()
collection.Dump(max3, ok3)
// 0 #int
// false #boolMaxBy · readonly · terminal
MaxBy returns the item whose key (produced by keyFn) is the largest. The second return value is false if the collection is empty.
Example: structs - highest score
type Player struct {
Name string
Score int
}
players := collection.New([]Player{
{Name: "Alice", Score: 10},
{Name: "Bob", Score: 25},
{Name: "Carol", Score: 18},
})
top, ok := collection.MaxBy(players, func(p Player) int {
return p.Score
})
collection.Dump(top, ok)
// #main.Player {
// +Name => "Bob" #string
// +Score => 25 #int
// }
// true #boolExample: strings - longest length
words := collection.New([]string{"go", "collection", "rocks"})
longest, ok := collection.MaxBy(words, func(s string) int {
return len(s)
})
collection.Dump(longest, ok)
// "collection" #string
// true #boolExample: empty collection
empty := collection.New([]int{})
maxVal, ok := collection.MaxBy(empty, func(v int) int { return v })
collection.Dump(maxVal, ok)
// 0 #int
// false #boolMedian · readonly · terminal
Median returns the statistical median of the numeric collection as float64. Returns (0, false) if the collection is empty.
Example: integers - odd number of items
c := collection.NewNumeric([]int{3, 1, 2})
median1, ok1 := c.Median()
collection.Dump(median1, ok1)
// 2.000000 #float64
// true #boolExample: integers - even number of items
c2 := collection.NewNumeric([]int{10, 2, 4, 6})
median2, ok2 := c2.Median()
collection.Dump(median2, ok2)
// 5.000000 #float64
// true #boolExample: floats
c3 := collection.NewNumeric([]float64{1.1, 9.9, 3.3})
median3, ok3 := c3.Median()
collection.Dump(median3, ok3)
// 3.300000 #float64
// true #boolExample: integers - empty numeric collection
c4 := collection.NewNumeric([]int{})
median4, ok4 := c4.Median()
collection.Dump(median4, ok4)
// 0.000000 #float64
// false #boolMin · readonly · terminal
Min returns the smallest numeric item in the collection. The second return value is false if the collection is empty.
Example: integers
c := collection.NewNumeric([]int{3, 1, 2})
min, ok := c.Min()
collection.Dump(min, ok)
// 1 #int
// true #boolExample: floats
c2 := collection.NewNumeric([]float64{2.5, 9.1, 1.2})
min2, ok2 := c2.Min()
collection.Dump(min2, ok2)
// 1.200000 #float64
// true #boolExample: integers - empty collection
empty := collection.NewNumeric([]int{})
min3, ok3 := empty.Min()
collection.Dump(min3, ok3)
// 0 #int
// false #boolMinBy · readonly · terminal
MinBy returns the item whose key (produced by keyFn) is the smallest. The second return value is false if the collection is empty.
Example: structs - smallest age
type User struct {
Name string
Age int
}
users := collection.New([]User{
{Name: "Alice", Age: 30},
{Name: "Bob", Age: 25},
{Name: "Carol", Age: 40},
})
minUser, ok := collection.MinBy(users, func(u User) int {
return u.Age
})
collection.Dump(minUser, ok)
// #main.User {
// +Name => "Bob" #string
// +Age => 25 #int
// }
// true #boolExample: strings - shortest length
words := collection.New([]string{"apple", "fig", "banana"})
shortest, ok := collection.MinBy(words, func(s string) int {
return len(s)
})
collection.Dump(shortest, ok)
// "fig" #string
// true #boolExample: empty collection
empty := collection.New([]int{})
minVal, ok := collection.MinBy(empty, func(v int) int { return v })
collection.Dump(minVal, ok)
// 0 #int
// false #boolMode · readonly · terminal
Mode returns the most frequent numeric value(s) in the collection. If multiple values tie for highest frequency, all are returned in first-seen order.
Example: integers – single mode
c := collection.NewNumeric([]int{1, 2, 2, 3})
mode := c.Mode()
collection.Dump(mode)
// #[]int [
// 0 => 2 #int
// ]Example: integers – tie for mode
c2 := collection.NewNumeric([]int{1, 2, 1, 2})
mode2 := c2.Mode()
collection.Dump(mode2)
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// ]Example: floats
c3 := collection.NewNumeric([]float64{1.1, 2.2, 1.1, 3.3})
mode3 := c3.Mode()
collection.Dump(mode3)
// #[]float64 [
// 0 => 1.100000 #float64
// ]Example: integers - empty collection
empty := collection.NewNumeric([]int{})
mode4 := empty.Mode()
collection.Dump(mode4)
// []int(nil)Reduce · readonly · terminal
Reduce collapses the collection into a single accumulated value. The accumulator has the same type T as the collection's elements.
Example: integers - sum
sum := collection.New([]int{1, 2, 3}).Reduce(0, func(acc, n int) int {
return acc + n
})
collection.Dump(sum)
// 6 #intExample: strings
joined := collection.New([]string{"a", "b", "c"}).Reduce("", func(acc, s string) string {
return acc + s
})
collection.Dump(joined)
// "abc" #stringExample: structs
type Stats struct {
Count int
Sum int
}
stats := collection.New([]Stats{
{Count: 1, Sum: 10},
{Count: 1, Sum: 20},
{Count: 1, Sum: 30},
})
total := stats.Reduce(Stats{}, func(acc, s Stats) Stats {
acc.Count += s.Count
acc.Sum += s.Sum
return acc
})
collection.Dump(total)
// #main.Stats {
// +Count => 3 #int
// +Sum => 60 #int
// }Sum · readonly · terminal
Sum returns the sum of all numeric items in the NumericCollection. If the collection is empty, Sum returns the zero value of T.
Example: integers
c := collection.NewNumeric([]int{1, 2, 3})
total := c.Sum()
collection.Dump(total)
// 6 #intExample: floats
c2 := collection.NewNumeric([]float64{1.5, 2.5})
total2 := c2.Sum()
collection.Dump(total2)
// 4.000000 #float64Example: integers - empty collection
c3 := collection.NewNumeric([]int{})
total3 := c3.Sum()
collection.Dump(total3)
// 0 #intConstruction
Clone · immutable · chainable
Clone returns a copy of the collection.
Example: basic cloning
c := collection.New([]int{1, 2, 3})
clone := c.Clone()
clone = clone.Append(4)
collection.Dump(c.Items())
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// ]
collection.Dump(clone.Items())
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// 3 => 4 #int
// ]Example: branching pipelines
base := collection.New([]int{1, 2, 3, 4, 5})
evens := base.Clone().Filter(func(v int) bool {
return v%2 == 0
})
odds := base.Clone().Filter(func(v int) bool {
return v%2 != 0
})
collection.Dump(base.Items())
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// 3 => 4 #int
// 4 => 5 #int
// ]
collection.Dump(evens.Items())
// #[]int [
// 0 => 2 #int
// 1 => 4 #int
// ]
collection.Dump(odds.Items())
// #[]int [
// 0 => 1 #int
// 1 => 3 #int
// 2 => 5 #int
// ]New · immutable · chainable
New creates a new Collection from the provided slice and borrows it.
NewNumeric · immutable · chainable
NewNumeric wraps a slice of numeric types in a NumericCollection and borrows it.
Debugging
Dd · terminal
Dd prints items then terminates execution. Like Laravel's dd(), this is intended for debugging and should not be used in production control flow.
c := collection.New([]string{"a", "b"})
c.Dd()
// #[]string [
// 0 => "a" #string
// 1 => "b" #string
// ]
// Process finished with the exit code 1Dump · readonly · chainable
Dump prints items with godump and returns the same collection. This is a no-op on the collection itself and never panics.
Example: integers
c := collection.New([]int{1, 2, 3})
c.Dump()
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// ]Example: integers - chaining
collection.New([]int{1, 2, 3}).
Filter(func(v int) bool { return v > 1 }).
Dump()
// #[]int [
// 0 => 2 #int
// 1 => 3 #int
// ]Example: integers
c2 := collection.New([]int{1, 2, 3})
collection.Dump(c2.Items())
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// ]DumpStr · readonly · terminal
DumpStr returns the pretty-printed dump of the items as a string, without printing or exiting. Useful for logging, snapshot testing, and non-interactive debugging.
c := collection.New([]int{10, 20})
s := c.DumpStr()
fmt.Println(s)
// #[]int [
// 0 => 10 #int
// 1 => 20 #int
// ]Grouping
GroupBy · readonly · terminal
GroupBy partitions the collection into groups keyed by the value returned from keyFn.
Example: grouping integers by parity
values := []int{1, 2, 3, 4, 5}
groups := collection.GroupBy(
collection.New(values),
func(v int) string {
if v%2 == 0 {
return "even"
}
return "odd"
},
)
collection.Dump(groups["even"].Items())
// #[]int [
// 0 => 2 #int
// 1 => 4 #int
// ]
collection.Dump(groups["odd"].Items())
// #[]int [
// 0 => 1 #int
// 1 => 3 #int
// 2 => 5 #int
// ]Example: grouping structs by field
type User struct {
ID int
Role string
}
users := []User{
{ID: 1, Role: "admin"},
{ID: 2, Role: "user"},
{ID: 3, Role: "admin"},
}
groups2 := collection.GroupBy(
collection.New(users),
func(u User) string { return u.Role },
)
collection.Dump(groups2["admin"].Items())
// #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// +Role => "admin" #string
// }
// 1 => #main.User {
// +ID => 3 #int
// +Role => "admin" #string
// }
// ]
collection.Dump(groups2["user"].Items())
// #[]main.User [
// 0 => #main.User {
// +ID => 2 #int
// +Role => "user" #string
// }
// ]GroupBySlice · readonly · terminal
GroupBySlice partitions the collection into groups keyed by the value returned from keyFn.
Example: grouping integers by parity
values := []int{1, 2, 3, 4, 5}
groups := collection.GroupBySlice(
collection.New(values),
func(v int) string {
if v%2 == 0 {
return "even"
}
return "odd"
},
)
collection.Dump(groups["even"])
// #[]int [
// 0 => 2 #int
// 1 => 4 #int
// ]
collection.Dump(groups["odd"])
// #[]int [
// 0 => 1 #int
// 1 => 3 #int
// 2 => 5 #int
// ]Example: grouping structs by field
type User struct {
ID int
Role string
}
users := []User{
{ID: 1, Role: "admin"},
{ID: 2, Role: "user"},
{ID: 3, Role: "admin"},
}
groups2 := collection.GroupBySlice(
collection.New(users),
func(u User) string { return u.Role },
)
collection.Dump(groups2["admin"])
// #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// +Role => "admin" #string
// }
// 1 => #main.User {
// +ID => 3 #int
// +Role => "admin" #string
// }
// ]
collection.Dump(groups2["user"])
// #[]main.User [
// 0 => #main.User {
// +ID => 2 #int
// +Role => "user" #string
// }
// ]Maps
FromMap · immutable · chainable
FromMap materializes a map into a collection of key/value pairs.
Example: basic usage
m := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
c := collection.FromMap(m)
c.Sort(func(a, b collection.Pair[string, int]) bool {
return a.Key < b.Key
})
collection.Dump(c.Items())
// #[]collection.Pair[string,int] [
// 0 => #collection.Pair[string,int] {
// +Key => "a" #string
// +Value => 1 #int
// }
// 1 => #collection.Pair[string,int] {
// +Key => "b" #string
// +Value => 2 #int
// }
// 2 => #collection.Pair[string,int] {
// +Key => "c" #string
// +Value => 3 #int
// }
// ]Example: filtering map entries
type Config struct {
Enabled bool
Timeout int
}
configs := map[string]Config{
"router-1": {Enabled: true, Timeout: 30},
"router-2": {Enabled: false, Timeout: 10},
"router-3": {Enabled: true, Timeout: 45},
}
out := collection.
FromMap(configs).
Filter(func(p collection.Pair[string, Config]) bool {
return p.Value.Enabled
}).
Sort(func(a, b collection.Pair[string, Config]) bool {
return a.Key < b.Key
}).
Items()
collection.Dump(out)
// #[]collection.Pair[string,main.Config·1] [
// 0 => #collection.Pair[string,main.Config·1] {
// +Key => "router-1" #string
// +Value => #main.Config {
// +Enabled => true #bool
// +Timeout => 30 #int
// }
// }
// 1 => #collection.Pair[string,main.Config·1] {
// +Key => "router-3" #string
// +Value => #main.Config {
// +Enabled => true #bool
// +Timeout => 45 #int
// }
// }
// ]Example: map → collection → map
users := map[string]int{
"alice": 1,
"bob": 2,
}
c2 := collection.FromMap(users)
out2 := collection.ToMapKV(c2)
collection.Dump(out2)
// #map[string]int {
// alice => 1 #int
// bob => 2 #int
// }ToMap · readonly · terminal
ToMap reduces a collection into a map using the provided key and value selector functions.
Example: basic usage
users := []string{"alice", "bob", "carol"}
out := collection.ToMap(
collection.New(users),
func(name string) string { return name },
func(name string) int { return len(name) },
)
collection.Dump(out)
// #map[string]int {
// alice => 5 #int
// bob => 3 #int
// carol => 5 #int
// }Example: re-keying structs
type User struct {
ID int
Name string
}
users2 := []User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
}
byID := collection.ToMap(
collection.New(users2),
func(u User) int { return u.ID },
func(u User) User { return u },
)
collection.Dump(byID)
// #map[int]main.User {
// 1 => #main.User {
// +ID => 1 #int
// +Name => "Alice" #string
// }
// 2 => #main.User {
// +ID => 2 #int
// +Name => "Bob" #string
// }
// }ToMapKV · readonly · terminal
ToMapKV converts a collection of key/value pairs into a map.
Example: basic usage
m := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
c := collection.FromMap(m)
out := collection.ToMapKV(c)
collection.Dump(out)
// #map[string]int {
// a => 1 #int
// b => 2 #int
// c => 3 #int
// }Example: filtering before conversion
type Config struct {
Enabled bool
Timeout int
}
configs := map[string]Config{
"router-1": {Enabled: true, Timeout: 30},
"router-2": {Enabled: false, Timeout: 10},
"router-3": {Enabled: true, Timeout: 45},
}
c2 := collection.
FromMap(configs).
Filter(func(p collection.Pair[string, Config]) bool {
return p.Value.Enabled
})
out2 := collection.ToMapKV(c2)
collection.Dump(out2)
// #map[string]main.Config {
// router-1 => #main.Config {
// +Enabled => true #bool
// +Timeout => 30 #int
// }
// router-3 => #main.Config {
// +Enabled => true #bool
// +Timeout => 45 #int
// }
// }Ordering
After · immutable · chainable
After returns all items after the first element for which pred returns true. If no element matches, an empty collection is returned.
c := collection.New([]int{1, 2, 3, 4, 5})
c.After(func(v int) bool { return v == 3 }).Dump()
// #[]int [
// 0 => 4 #int
// 1 => 5 #int
// ]Before · immutable · chainable
Before returns a new collection containing all items that appear before the first element for which pred returns true.
Example: integers
c1 := collection.New([]int{1, 2, 3, 4, 5})
out1 := c1.Before(func(v int) bool { return v >= 3 })
collection.Dump(out1.Items())
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// ]Example: predicate never matches → whole collection returned
c2 := collection.New([]int{10, 20, 30})
out2 := c2.Before(func(v int) bool { return v == 99 })
collection.Dump(out2.Items())
// #[]int [
// 0 => 10 #int
// 1 => 20 #int
// 2 => 30 #int
// ]Example: structs: get all users before the first admin
type User struct {
Name string
Admin bool
}
c3 := collection.New([]User{
{Name: "Alice", Admin: false},
{Name: "Bob", Admin: false},
{Name: "Eve", Admin: true},
{Name: "Mallory", Admin: false},
})
out3 := c3.Before(func(u User) bool { return u.Admin })
collection.Dump(out3.Items())
// #[]main.User [
// 0 => #main.User {
// +Name => "Alice" #string
// +Admin => false #bool
// }
// 1 => #main.User {
// +Name => "Bob" #string
// +Admin => false #bool
// }
// ]Reverse · mutable · chainable
Reverse reverses the order of items in the collection in place and returns the same collection for chaining.
Example: integers
c := collection.New([]int{1, 2, 3, 4})
c.Reverse()
collection.Dump(c.Items())
// #[]int [
// 0 => 4 #int
// 1 => 3 #int
// 2 => 2 #int
// 3 => 1 #int
// ]Example: strings – chaining
out := collection.New([]string{"a", "b", "c"}).
Reverse().
Append("d").
Items()
collection.Dump(out)
// #[]string [
// 0 => "c" #string
// 1 => "b" #string
// 2 => "a" #string
// 3 => "d" #string
// ]Example: structs
type User struct {
ID int
}
users := collection.New([]User{
{ID: 1},
{ID: 2},
{ID: 3},
})
users.Reverse()
collection.Dump(users.Items())
// #[]main.User [
// 0 => #main.User {
// +ID => 3 #int
// }
// 1 => #main.User {
// +ID => 2 #int
// }
// 2 => #main.User {
// +ID => 1 #int
// }
// ]Shuffle · mutable · chainable
Shuffle shuffles the collection in place and returns the same collection.
Example: integers
c := collection.New([]int{1, 2, 3, 4, 5})
c.Shuffle()
collection.Dump(c.Items())Example: strings – chaining
out2 := collection.New([]string{"a", "b", "c"}).
Shuffle().
Append("d").
Items()
collection.Dump(out2)Example: structs
type User struct {
ID int
}
users := collection.New([]User{
{ID: 1},
{ID: 2},
{ID: 3},
{ID: 4},
})
users.Shuffle()
collection.Dump(users.Items())Sort · mutable · chainable
Sort sorts the collection in place using the provided comparison function and returns the same collection for chaining.
Example: integers
c := collection.New([]int{5, 1, 4, 2})
c.Sort(func(a, b int) bool { return a < b })
collection.Dump(c.Items())
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 4 #int
// 3 => 5 #int
// ]Example: strings (descending)
c2 := collection.New([]string{"apple", "banana", "cherry"})
c2.Sort(func(a, b string) bool { return a > b })
collection.Dump(c2.Items())
// #[]string [
// 0 => "cherry" #string
// 1 => "banana" #string
// 2 => "apple" #string
// ]Example: structs
type User struct {
Name string
Age int
}
users := collection.New([]User{
{Name: "Alice", Age: 30},
{Name: "Bob", Age: 25},
{Name: "Carol", Age: 40},
})
// Sort by age ascending
users.Sort(func(a, b User) bool {
return a.Age < b.Age
})
collection.Dump(users.Items())
// #[]main.User [
// 0 => #main.User {
// +Name => "Bob" #string
// +Age => 25 #int
// }
// 1 => #main.User {
// +Name => "Alice" #string
// +Age => 30 #int
// }
// 2 => #main.User {
// +Name => "Carol" #string
// +Age => 40 #int
// }
// ]Querying
All · readonly · terminal
All returns true if fn returns true for every item in the collection. If the collection is empty, All returns true (vacuously true).
Example: integers – all even
c := collection.New([]int{2, 4, 6})
allEven := c.All(func(v int) bool { return v%2 == 0 })
collection.Dump(allEven)
// true #boolExample: integers – not all even
c2 := collection.New([]int{2, 3, 4})
allEven2 := c2.All(func(v int) bool { return v%2 == 0 })
collection.Dump(allEven2)
// false #boolExample: strings – all non-empty
c3 := collection.New([]string{"a", "b", "c"})
allNonEmpty := c3.All(func(s string) bool { return s != "" })
collection.Dump(allNonEmpty)
// true #boolExample: empty collection (vacuously true)
empty := collection.New([]int{})
all := empty.All(func(v int) bool { return v > 0 })
collection.Dump(all)
// true #boolAny · readonly · terminal
Any returns true if at least one item satisfies fn.
c := collection.New([]int{1, 2, 3, 4})
has := c.Any(func(v int) bool { return v%2 == 0 }) // true
collection.Dump(has)
// true #boolAt · readonly · terminal
At returns the item at the given index and a boolean indicating whether the index was within bounds.
Example: integers
c := collection.New([]int{10, 20, 30})
v, ok := c.At(1)
collection.Dump(v, ok)
// 20 #int
// true #boolExample: out of bounds
v2, ok2 := c.At(10)
collection.Dump(v2, ok2)
// 0 #int
// false #boolExample: structs
type User struct {
ID int
Name string
}
users := collection.New([]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
})
u, ok3 := users.At(0)
collection.Dump(u, ok3)
// #main.User {
// +ID => 1 #int
// +Name => "Alice" #string
// }
// true #boolContains · readonly · terminal
Contains returns true if the collection contains the given value.
Example: integers
c := collection.New([]int{1, 2, 3, 4, 5})
hasTwo := collection.Contains(c, 2)
collection.Dump(hasTwo)
// true #boolExample: strings
c2 := collection.New([]string{"apple", "banana", "cherry"})
hasBanana := collection.Contains(c2, "banana")
collection.Dump(hasBanana)
// true #boolFirst · readonly · terminal
First returns the first element in the collection. If the collection is empty, ok will be false.
Example: integers
c := collection.New([]int{10, 20, 30})
v, ok := c.First()
collection.Dump(v, ok)
// 10 #int
// true #boolExample: strings
c2 := collection.New([]string{"alpha", "beta", "gamma"})
v2, ok2 := c2.First()
collection.Dump(v2, ok2)
// "alpha" #string
// true #boolExample: structs
type User struct {
ID int
Name string
}
users := collection.New([]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
})
u, ok3 := users.First()
collection.Dump(u, ok3)
// #main.User {
// +ID => 1 #int
// +Name => "Alice" #string
// }
// true #boolExample: integers - empty collection
c3 := collection.New([]int{})
v3, ok4 := c3.First()
collection.Dump(v3, ok4)
// 0 #int
// false #boolFirstWhere · readonly · terminal
FirstWhere returns the first item in the collection for which the provided predicate function returns true. If no items match, ok=false is returned along with the zero value of T.
nums := collection.New([]int{1, 2, 3, 4, 5})
v, ok := nums.FirstWhere(func(n int) bool {
return n%2 == 0
})
collection.Dump(v, ok)
// 2 #int
// true #bool
v, ok = nums.FirstWhere(func(n int) bool {
return n > 10
})
collection.Dump(v, ok)
// 0 #int
// false #boolIndexWhere · readonly · terminal
IndexWhere returns the index of the first item in the collection for which the provided predicate function returns true. If no item matches, it returns (0, false).
Example: integers
c := collection.New([]int{10, 20, 30, 40})
idx, ok := c.IndexWhere(func(v int) bool { return v == 30 })
collection.Dump(idx, ok)
// 2 #int
// true #boolExample: not found
idx2, ok2 := c.IndexWhere(func(v int) bool { return v == 99 })
collection.Dump(idx2, ok2)
// 0 #int
// false #boolExample: structs
type User struct {
ID int
Name string
}
users := collection.New([]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
{ID: 3, Name: "Carol"},
})
idx3, ok3 := users.IndexWhere(func(u User) bool {
return u.Name == "Bob"
})
collection.Dump(idx3, ok3)
// 1 #int
// true #boolIsEmpty · readonly · terminal
IsEmpty returns true if the collection has no items.
Example: integers (non-empty)
c := collection.New([]int{1, 2, 3})
empty := c.IsEmpty()
collection.Dump(empty)
// false #boolExample: strings (empty)
c2 := collection.New([]string{})
empty2 := c2.IsEmpty()
collection.Dump(empty2)
// true #boolExample: structs (non-empty)
type User struct {
ID int
Name string
}
users := collection.New([]User{
{ID: 1, Name: "Alice"},
})
empty3 := users.IsEmpty()
collection.Dump(empty3)
// false #boolExample: structs (empty)
none := collection.New([]User{})
empty4 := none.IsEmpty()
collection.Dump(empty4)
// true #boolLast · readonly · terminal
Last returns the last element in the collection. If the collection is empty, ok will be false.
Example: integers
c := collection.New([]int{10, 20, 30})
v, ok := c.Last()
collection.Dump(v, ok)
// 30 #int
// true #boolExample: strings
c2 := collection.New([]string{"alpha", "beta", "gamma"})
v2, ok2 := c2.Last()
collection.Dump(v2, ok2)
// "gamma" #string
// true #boolExample: structs
type User struct {
ID int
Name string
}
users := collection.New([]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
{ID: 3, Name: "Charlie"},
})
u, ok3 := users.Last()
collection.Dump(u, ok3)
// #main.User {
// +ID => 3 #int
// +Name => "Charlie" #string
// }
// true #boolExample: empty collection
c3 := collection.New([]int{})
v3, ok4 := c3.Last()
collection.Dump(v3, ok4)
// 0 #int
// false #boolLastWhere · readonly · terminal
LastWhere returns the last element in the collection that satisfies the predicate fn. If fn is nil, LastWhere returns the final element in the underlying slice. If the collection is empty or no element matches, ok will be false.
Example: integers
c := collection.New([]int{1, 2, 3, 4})
v, ok := c.LastWhere(func(v int, i int) bool {
return v < 3
})
collection.Dump(v, ok)
// 2 #int
// true #boolExample: integers without predicate (equivalent to Last())
c2 := collection.New([]int{10, 20, 30, 40})
v2, ok2 := c2.LastWhere(nil)
collection.Dump(v2, ok2)
// 40 #int
// true #boolExample: strings
c3 := collection.New([]string{"alpha", "beta", "gamma", "delta"})
v3, ok3 := c3.LastWhere(func(s string, i int) bool {
return strings.HasPrefix(s, "g")
})
collection.Dump(v3, ok3)
// "gamma" #string
// true #boolExample: structs
type User struct {
ID int
Name string
}
users := collection.New([]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
{ID: 3, Name: "Alex"},
{ID: 4, Name: "Brian"},
})
u, ok4 := users.LastWhere(func(u User, i int) bool {
return strings.HasPrefix(u.Name, "A")
})
collection.Dump(u, ok4)
// #main.User {
// +ID => 3 #int
// +Name => "Alex" #string
// }
// true #boolExample: no matching element
c4 := collection.New([]int{5, 6, 7})
v4, ok5 := c4.LastWhere(func(v int, i int) bool {
return v > 10
})
collection.Dump(v4, ok5)
// 0 #int
// false #boolExample: empty collection
c5 := collection.New([]int{})
v5, ok6 := c5.LastWhere(nil)
collection.Dump(v5, ok6)
// 0 #int
// false #boolNone · readonly · terminal
None returns true if fn returns false for every item in the collection. If the collection is empty, None returns true.
Example: integers – none even
c := collection.New([]int{1, 3, 5})
noneEven := c.None(func(v int) bool { return v%2 == 0 })
collection.Dump(noneEven)
// true #boolExample: integers – some even
c2 := collection.New([]int{1, 2, 3})
noneEven2 := c2.None(func(v int) bool { return v%2 == 0 })
collection.Dump(noneEven2)
// false #boolExample: empty collection
empty := collection.New([]int{})
none := empty.None(func(v int) bool { return v > 0 })
collection.Dump(none)
// true #boolSerialization
ToJSON · readonly · terminal
ToJSON converts the collection's items into a compact JSON string.
pj1 := collection.New([]string{"a", "b"})
out1, _ := pj1.ToJSON()
fmt.Println(out1)
// ["a","b"]ToPrettyJSON · readonly · terminal
ToPrettyJSON converts the collection's items into a human-readable, indented JSON string.
pj1 := collection.New([]string{"a", "b"})
out1, _ := pj1.ToPrettyJSON()
fmt.Println(out1)
// [
// "a",
// "b"
// ]Set Operations
Difference · immutable · chainable
Difference returns a new collection containing elements from the first collection that are not present in the second. Order follows the first collection, and duplicates are removed.
Example: integers
a := collection.New([]int{1, 2, 2, 3, 4})
b := collection.New([]int{2, 4})
out := collection.Difference(a, b)
collection.Dump(out.Items())
// #[]int [
// 0 => 1 #int
// 1 => 3 #int
// ]Example: strings
left := collection.New([]string{"apple", "banana", "cherry"})
right := collection.New([]string{"banana"})
out2 := collection.Difference(left, right)
collection.Dump(out2.Items())
// #[]string [
// 0 => "apple" #string
// 1 => "cherry" #string
// ]Example: structs
type User struct {
ID int
Name string
}
groupA := collection.New([]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
{ID: 3, Name: "Carol"},
})
groupB := collection.New([]User{
{ID: 2, Name: "Bob"},
})
out3 := collection.Difference(groupA, groupB)
collection.Dump(out3.Items())
// #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// +Name => "Alice" #string
// }
// 1 => #main.User {
// +ID => 3 #int
// +Name => "Carol" #string
// }
// ]Intersect · immutable · chainable
Intersect returns a new collection containing elements from the second collection that are also present in the first.
Example: integers
a := collection.New([]int{1, 2, 2, 3, 4})
b := collection.New([]int{2, 4, 4, 5})
out := collection.Intersect(a, b)
collection.Dump(out.Items())
// #[]int [
// 0 => 2 #int
// 1 => 4 #int
// 2 => 4 #int
// ]Example: strings
left := collection.New([]string{"apple", "banana", "cherry"})
right := collection.New([]string{"banana", "date", "cherry", "banana"})
out2 := collection.Intersect(left, right)
collection.Dump(out2.Items())
// #[]string [
// 0 => "banana" #string
// 1 => "cherry" #string
// 2 => "banana" #string
// ]Example: structs
type User struct {
ID int
Name string
}
groupA := collection.New([]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
{ID: 3, Name: "Carol"},
})
groupB := collection.New([]User{
{ID: 2, Name: "Bob"},
{ID: 3, Name: "Carol"},
{ID: 4, Name: "Dave"},
})
out3 := collection.Intersect(groupA, groupB)
collection.Dump(out3.Items())
// #[]main.User [
// 0 => #main.User {
// +ID => 2 #int
// +Name => "Bob" #string
// }
// 1 => #main.User {
// +ID => 3 #int
// +Name => "Carol" #string
// }
// ]SymmetricDifference · immutable · chainable
SymmetricDifference returns a new collection containing elements that appear in exactly one of the two collections. Order follows the first collection for its unique items, then the second for its unique items. Duplicates are removed.
Example: integers
a := collection.New([]int{1, 2, 3, 3})
b := collection.New([]int{3, 4, 4, 5})
out := collection.SymmetricDifference(a, b)
collection.Dump(out.Items())
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 4 #int
// 3 => 5 #int
// ]Example: strings
left := collection.New([]string{"apple", "banana"})
right := collection.New([]string{"banana", "date"})
out2 := collection.SymmetricDifference(left, right)
collection.Dump(out2.Items())
// #[]string [
// 0 => "apple" #string
// 1 => "date" #string
// ]Example: structs
type User struct {
ID int
Name string
}
groupA := collection.New([]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
})
groupB := collection.New([]User{
{ID: 2, Name: "Bob"},
{ID: 3, Name: "Carol"},
})
out3 := collection.SymmetricDifference(groupA, groupB)
collection.Dump(out3.Items())
// #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// +Name => "Alice" #string
// }
// 1 => #main.User {
// +ID => 3 #int
// +Name => "Carol" #string
// }
// ]Union · immutable · chainable
Union returns a new collection containing the unique elements from both collections. Items from the first collection are kept in order, followed by items from the second that were not already present.
Example: integers
a := collection.New([]int{1, 2, 2, 3})
b := collection.New([]int{3, 4, 4, 5})
out := collection.Union(a, b)
collection.Dump(out.Items())
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// 3 => 4 #int
// 4 => 5 #int
// ]Example: strings
left := collection.New([]string{"apple", "banana"})
right := collection.New([]string{"banana", "date"})
out2 := collection.Union(left, right)
collection.Dump(out2.Items())
// #[]string [
// 0 => "apple" #string
// 1 => "banana" #string
// 2 => "date" #string
// ]Example: structs
type User struct {
ID int
Name string
}
groupA := collection.New([]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
})
groupB := collection.New([]User{
{ID: 2, Name: "Bob"},
{ID: 3, Name: "Carol"},
})
out3 := collection.Union(groupA, groupB)
collection.Dump(out3.Items())
// #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// +Name => "Alice" #string
// }
// 1 => #main.User {
// +ID => 2 #int
// +Name => "Bob" #string
// }
// 2 => #main.User {
// +ID => 3 #int
// +Name => "Carol" #string
// }
// ]Unique · immutable · chainable
Unique returns a new collection with duplicate items removed, based on the equality function eq. The first occurrence of each unique value is kept, and order is preserved.
Example: integers
c1 := collection.New([]int{1, 2, 2, 3, 4, 4, 5})
out1 := c1.Unique(func(a, b int) bool { return a == b })
collection.Dump(out1.Items())
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// 3 => 4 #int
// 4 => 5 #int
// ]Example: strings (case-insensitive uniqueness)
c2 := collection.New([]string{"A", "a", "B", "b", "A"})
out2 := c2.Unique(func(a, b string) bool {
return strings.EqualFold(a, b)
})
collection.Dump(out2.Items())
// #[]string [
// 0 => "A" #string
// 1 => "B" #string
// ]Example: structs (unique by ID)
type User struct {
ID int
Name string
}
c3 := collection.New([]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
{ID: 1, Name: "Alice Duplicate"},
})
out3 := c3.Unique(func(a, b User) bool {
return a.ID == b.ID
})
collection.Dump(out3.Items())
// #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// +Name => "Alice" #string
// }
// 1 => #main.User {
// +ID => 2 #int
// +Name => "Bob" #string
// }
// ]UniqueBy · immutable · chainable
UniqueBy returns a new collection containing only the first occurrence of each element as determined by keyFn.
Example: structs – unique by ID
type User struct {
ID int
Name string
}
users := collection.New([]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
{ID: 1, Name: "Alice Duplicate"},
})
out := collection.UniqueBy(users, func(u User) int { return u.ID })
collection.Dump(out.Items())
// #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// +Name => "Alice" #string
// }
// 1 => #main.User {
// +ID => 2 #int
// +Name => "Bob" #string
// }
// ]Example: strings – case-insensitive uniqueness
values := collection.New([]string{"A", "a", "B", "b", "A"})
out2 := collection.UniqueBy(values, func(s string) string {
return strings.ToLower(s)
})
collection.Dump(out2.Items())
// #[]string [
// 0 => "A" #string
// 1 => "B" #string
// ]Example: integers – identity key
nums := collection.New([]int{3, 1, 2, 1, 3})
out3 := collection.UniqueBy(nums, func(v int) int { return v })
collection.Dump(out3.Items())
// #[]int [
// 0 => 3 #int
// 1 => 1 #int
// 2 => 2 #int
// ]UniqueComparable · immutable · chainable
UniqueComparable returns a new collection with duplicate comparable items removed. The first occurrence of each value is kept, and order is preserved. This is a faster, allocation-friendly path for comparable types.
Example: integers
c := collection.New([]int{1, 2, 2, 3, 4, 4, 5})
out := collection.UniqueComparable(c)
collection.Dump(out.Items())
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// 3 => 4 #int
// 4 => 5 #int
// ]Example: strings
c2 := collection.New([]string{"A", "a", "B", "B"})
out2 := collection.UniqueComparable(c2)
collection.Dump(out2.Items())
// #[]string [
// 0 => "A" #string
// 1 => "a" #string
// 2 => "B" #string
// ]Slicing
Chunk · readonly · terminal
Chunk splits the collection into chunks of the given size. The final chunk may be smaller if len(items) is not divisible by size.
Example: integers
c := collection.New([]int{1, 2, 3, 4, 5}).Chunk(2)
collection.Dump(c)
// #[][]int [
// 0 => #[]int [
// 0 => 1 #int
// 1 => 2 #int
// ]
// 1 => #[]int [
// 0 => 3 #int
// 1 => 4 #int
// ]
// 2 => #[]int [
// 0 => 5 #int
// ]
//]Example: structs
type User struct {
ID int
Name string
}
users := []User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
{ID: 3, Name: "Carol"},
{ID: 4, Name: "Dave"},
}
userChunks := collection.New(users).Chunk(2)
collection.Dump(userChunks)
// Dump output will show [][]User grouped in size-2 chunks, e.g.:
// #[][]main.User [
// 0 => #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// +Name => "Alice" #string
// }
// 1 => #main.User {
// +ID => 2 #int
// +Name => "Bob" #string
// }
// ]
// 1 => #[]main.User [
// 0 => #main.User {
// +ID => 3 #int
// +Name => "Carol" #string
// }
// 1 => #main.User {
// +ID => 4 #int
// +Name => "Dave" #string
// }
// ]
//]Filter · mutable · chainable
Filter keeps only the elements for which fn returns true. This method mutates the collection in place and returns the same instance.
Example: integers
c := collection.New([]int{1, 2, 3, 4})
c.Filter(func(v int) bool {
return v%2 == 0
})
collection.Dump(c.Items())
// #[]int [
// 0 => 2 #int
// 1 => 4 #int
// ]Example: strings
c2 := collection.New([]string{"apple", "banana", "cherry", "avocado"})
c2.Filter(func(v string) bool {
return strings.HasPrefix(v, "a")
})
collection.Dump(c2.Items())
// #[]string [
// 0 => "apple" #string
// 1 => "avocado" #string
// ]Example: structs
type User struct {
ID int
Name string
}
users := collection.New([]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
{ID: 3, Name: "Andrew"},
{ID: 4, Name: "Carol"},
})
users.Filter(func(u User) bool {
return strings.HasPrefix(u.Name, "A")
})
collection.Dump(users.Items())
// #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// +Name => "Alice" #string
// }
// 1 => #main.User {
// +ID => 3 #int
// +Name => "Andrew" #string
// }
// ]Partition · immutable · terminal
Partition splits the collection into two new collections based on predicate fn. The first collection contains items where fn returns true; the second contains items where fn returns false. Order is preserved within each partition.
Example: integers - even/odd
nums := collection.New([]int{1, 2, 3, 4, 5})
evens, odds := nums.Partition(func(n int) bool {
return n%2 == 0
})
collection.Dump(evens.Items(), odds.Items())
// #[]int [
// 0 => 2 #int
// 1 => 4 #int
// ]
// #[]int [
// 0 => 1 #int
// 1 => 3 #int
// 2 => 5 #int
// ]Example: strings - prefix match
words := collection.New([]string{"go", "gopher", "rust", "ruby"})
goWords, other := words.Partition(func(s string) bool {
return strings.HasPrefix(s, "go")
})
collection.Dump(goWords.Items(), other.Items())
// #[]string [
// 0 => "go" #string
// 1 => "gopher" #string
// ]
// #[]string [
// 0 => "rust" #string
// 1 => "ruby" #string
// ]Example: structs - active vs inactive
type User struct {
Name string
Active bool
}
users := collection.New([]User{
{Name: "Alice", Active: true},
{Name: "Bob", Active: false},
{Name: "Carol", Active: true},
})
active, inactive := users.Partition(func(u User) bool {
return u.Active
})
collection.Dump(active.Items(), inactive.Items())
// #[]main.User [
// 0 => #main.User {
// +Name => "Alice" #string
// +Active => true #bool
// }
// 1 => #main.User {
// +Name => "Carol" #string
// +Active => true #bool
// }
// ]
// #[]main.User [
// 0 => #main.User {
// +Name => "Bob" #string
// +Active => false #bool
// }
// ]Pop · mutable · terminal
Pop removes and returns the last item in the collection.
Example: integers
c := collection.New([]int{1, 2, 3})
item, ok := c.Pop()
collection.Dump(item, ok, c.Items())
// 3 #int
// true #bool
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// ]Example: strings
c2 := collection.New([]string{"a", "b", "c"})
item2, ok2 := c2.Pop()
collection.Dump(item2, ok2, c2.Items())
// "c" #string
// true #bool
// #[]string [
// 0 => "a" #string
// 1 => "b" #string
// ]Example: structs
type User struct {
ID int
Name string
}
users := collection.New([]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
})
item3, ok3 := users.Pop()
collection.Dump(item3, ok3, users.Items())
// #main.User {
// +ID => 2 #int
// +Name => "Bob" #string
// }
// true #bool
// #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// +Name => "Alice" #string
// }
// ]Example: empty collection
empty := collection.New([]int{})
item4, ok4 := empty.Pop()
collection.Dump(item4, ok4, empty.Items())
// 0 #int
// false #bool
// #[]int [
// ]PopN · mutable · terminal
PopN removes and returns the last n items in original order.
Example: integers – pop 2
c := collection.New([]int{1, 2, 3, 4})
popped := c.PopN(2)
collection.Dump(popped, c.Items())
// #[]int [
// 0 => 3 #int
// 1 => 4 #int
// ]
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// ]Example: strings – pop 1
c2 := collection.New([]string{"a", "b", "c"})
popped2 := c2.PopN(1)
collection.Dump(popped2, c2.Items())
// #[]string [
// 0 => "c" #string
// ]
// #[]string [
// 0 => "a" #string
// 1 => "b" #string
// ]Example: structs – pop 2
type User struct {
ID int
Name string
}
users := collection.New([]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
{ID: 3, Name: "Carol"},
})
popped3 := users.PopN(2)
collection.Dump(popped3, users.Items())
// #[]main.User [
// 0 => #main.User {
// +ID => 2 #int
// +Name => "Bob" #string
// }
// 1 => #main.User {
// +ID => 3 #int
// +Name => "Carol" #string
// }
// ]
// #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// +Name => "Alice" #string
// }
// ]Example: integers - n <= 0 → returns nil, no change
c3 := collection.New([]int{1, 2, 3})
popped4 := c3.PopN(0)
collection.Dump(popped4, c3.Items())
// []int(nil)
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// ]Example: strings - n exceeds length → all items popped, rest empty
c4 := collection.New([]string{"x", "y"})
popped5 := c4.PopN(10)
collection.Dump(popped5, c4.Items())
// #[]string [
// 0 => "x" #string
// 1 => "y" #string
// ]
// #[]string [
// ]Skip · immutable · chainable
Skip returns a new collection with the first n items skipped. If n is less than or equal to zero, Skip returns the full collection. If n is greater than or equal to the collection length, Skip returns an empty collection.
Example: integers
c := collection.New([]int{1, 2, 3, 4, 5})
out := c.Skip(2)
collection.Dump(out.Items())
// #[]int [
// 0 => 3 #int
// 1 => 4 #int
// 2 => 5 #int
// ]Example: skip none
out2 := c.Skip(0)
collection.Dump(out2.Items())
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// 3 => 4 #int
// 4 => 5 #int
// ]Example: skip all
out3 := c.Skip(10)
collection.Dump(out3.Items())
// #[]int [
// ]Example: structs
type User struct {
ID int
}
users := collection.New([]User{
{ID: 1},
{ID: 2},
{ID: 3},
})
out4 := users.Skip(1)
collection.Dump(out4.Items())
// #[]main.User [
// 0 => #main.User {
// +ID => 2 #int
// }
// 1 => #main.User {
// +ID => 3 #int
// }
// ]SkipLast · immutable · chainable
SkipLast returns a new collection with the last n items skipped. If n is less than or equal to zero, SkipLast returns the full collection. If n is greater than or equal to the collection length, SkipLast returns an empty collection.
Example: integers
c := collection.New([]int{1, 2, 3, 4, 5})
out := c.SkipLast(2)
collection.Dump(out.Items())
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// ]Example: skip none
out2 := c.SkipLast(0)
collection.Dump(out2.Items())
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// 3 => 4 #int
// 4 => 5 #int
// ]Example: skip all
out3 := c.SkipLast(10)
collection.Dump(out3.Items())
// #[]int [
// ]Example: structs
type User struct {
ID int
}
users := collection.New([]User{
{ID: 1},
{ID: 2},
{ID: 3},
})
out4 := users.SkipLast(1)
collection.Dump(out4.Items())
// #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// }
// 1 => #main.User {
// +ID => 2 #int
// }
// ]Take · immutable · chainable
Take returns a new collection containing the first n items when n > 0, or the last |n| items when n < 0.
Example: integers - take first 3
c1 := collection.New([]int{0, 1, 2, 3, 4, 5})
out1 := c1.Take(3)
collection.Dump(out1.Items())
// #[]int [
// 0 => 0 #int
// 1 => 1 #int
// 2 => 2 #int
// ]Example: integers - take last 2 (negative n)
c2 := collection.New([]int{0, 1, 2, 3, 4, 5})
out2 := c2.Take(-2)
collection.Dump(out2.Items())
// #[]int [
// 0 => 4 #int
// 1 => 5 #int
// ]Example: integers - n exceeds length → whole collection
c3 := collection.New([]int{10, 20})
out3 := c3.Take(10)
collection.Dump(out3.Items())
// #[]int [
// 0 => 10 #int
// 1 => 20 #int
// ]Example: integers - zero → empty
c4 := collection.New([]int{1, 2, 3})
out4 := c4.Take(0)
collection.Dump(out4.Items())
// #[]int [
// ]TakeLast · immutable · chainable
TakeLast returns a new collection containing the last n items. If n is less than or equal to zero, TakeLast returns an empty collection. If n is greater than or equal to the collection length, TakeLast returns the full collection.
Example: integers
c := collection.New([]int{1, 2, 3, 4, 5})
out := c.TakeLast(2)
collection.Dump(out.Items())
// #[]int [
// 0 => 4 #int
// 1 => 5 #int
// ]Example: take none
out2 := c.TakeLast(0)
collection.Dump(out2.Items())
// #[]int [
// ]Example: take all
out3 := c.TakeLast(10)
collection.Dump(out3.Items())
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// 3 => 4 #int
// 4 => 5 #int
// ]Example: structs
type User struct {
ID int
}
users := collection.New([]User{
{ID: 1},
{ID: 2},
{ID: 3},
})
out4 := users.TakeLast(1)
collection.Dump(out4.Items())
// #[]main.User [
// 0 => #main.User {
// +ID => 3 #int
// }
// ]TakeUntil · immutable · chainable
TakeUntil returns items until the first element equals value. The matching item is NOT included.
Example: integers - stop at value 3
c4 := collection.New([]int{1, 2, 3, 4})
out4 := collection.TakeUntil(c4, 3)
collection.Dump(out4.Items())
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// ]Example: strings - value never appears → full slice
c5 := collection.New([]string{"a", "b", "c"})
out5 := collection.TakeUntil(c5, "x")
collection.Dump(out5.Items())
// #[]string [
// 0 => "a" #string
// 1 => "b" #string
// 2 => "c" #string
// ]Example: integers - match is first item → empty result
c6 := collection.New([]int{9, 10, 11})
out6 := collection.TakeUntil(c6, 9)
collection.Dump(out6.Items())
// #[]int [
// ]TakeUntilFn · immutable · chainable
TakeUntilFn returns items until the predicate function returns true. The matching item is NOT included.
Example: integers - stop when value >= 3
c1 := collection.New([]int{1, 2, 3, 4})
out1 := c1.TakeUntilFn(func(v int) bool { return v >= 3 })
collection.Dump(out1.Items())
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// ]Example: integers - predicate immediately true → empty result
c2 := collection.New([]int{10, 20, 30})
out2 := c2.TakeUntilFn(func(v int) bool { return v < 50 })
collection.Dump(out2.Items())
// #[]int [
// ]Example: integers - no match → full list returned
c3 := collection.New([]int{1, 2, 3})
out3 := c3.TakeUntilFn(func(v int) bool { return v == 99 })
collection.Dump(out3.Items())
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// ]Window · allocates · chainable
Window returns overlapping (or stepped) windows of the collection. Each window is a slice of length size; iteration advances by step (default 1 if step <= 0). Windows that are shorter than size are omitted.
Example: integers - step 1
nums := collection.New([]int{1, 2, 3, 4, 5})
win := collection.Window(nums, 3, 1)
collection.Dump(win.Items())
// #[][]int [
// 0 => #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// ]
// 1 => #[]int [
// 0 => 2 #int
// 1 => 3 #int
// 2 => 4 #int
// ]
// 2 => #[]int [
// 0 => 3 #int
// 1 => 4 #int
// 2 => 5 #int
// ]
// ]Example: strings - step 2
words := collection.New([]string{"a", "b", "c", "d", "e"})
win2 := collection.Window(words, 2, 2)
collection.Dump(win2.Items())
// #[][]string [
// 0 => #[]string [
// 0 => "a" #string
// 1 => "b" #string
// ]
// 1 => #[]string [
// 0 => "c" #string
// 1 => "d" #string
// ]
// ]Example: structs
type Point struct {
X int
Y int
}
points := collection.New([]Point{
{X: 0, Y: 0},
{X: 1, Y: 1},
{X: 2, Y: 4},
{X: 3, Y: 9},
})
win3 := collection.Window(points, 2, 1)
collection.Dump(win3.Items())
// #[][]main.Point [
// 0 => #[]main.Point [
// 0 => #main.Point {
// +X => 0 #int
// +Y => 0 #int
// }
// 1 => #main.Point {
// +X => 1 #int
// +Y => 1 #int
// }
// ]
// 1 => #[]main.Point [
// 0 => #main.Point {
// +X => 1 #int
// +Y => 1 #int
// }
// 1 => #main.Point {
// +X => 2 #int
// +Y => 4 #int
// }
// ]
// 2 => #[]main.Point [
// 0 => #main.Point {
// +X => 2 #int
// +Y => 4 #int
// }
// 1 => #main.Point {
// +X => 3 #int
// +Y => 9 #int
// }
// ]
// ]Transformation
Append · immutable · chainable
Append returns a new collection with the given values appended.
Example: integers
c := collection.New([]int{1, 2})
c.Append(3, 4).Dump()
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// 3 => 4 #int
// ]Example: structs
type User struct {
ID int
Name string
}
users := collection.New([]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
})
users.Append(
User{ID: 3, Name: "Carol"},
User{ID: 4, Name: "Dave"},
).Dump()
// #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// +Name => "Alice" #string
// }
// 1 => #main.User {
// +ID => 2 #int
// +Name => "Bob" #string
// }
// 2 => #main.User {
// +ID => 3 #int
// +Name => "Carol" #string
// }
// 3 => #main.User {
// +ID => 4 #int
// +Name => "Dave" #string
// }
// ]Concat · mutable · chainable
Concat appends the values from the given slice onto the end of the collection,
c := collection.New([]string{"John Doe"})
concatenated := c.
Concat([]string{"Jane Doe"}).
Concat([]string{"Johnny Doe"}).
Items()
collection.Dump(concatenated)
// #[]string [
// 0 => "John Doe" #string
// 1 => "Jane Doe" #string
// 2 => "Johnny Doe" #string
// ]Each · readonly · chainable
Each runs fn for every item in the collection and returns the same collection, so it can be used in chains for side effects (logging, debugging, etc.).
Example: integers
c := collection.New([]int{1, 2, 3})
sum := 0
c.Each(func(v int) {
sum += v
})
collection.Dump(sum)
// 6 #intExample: strings
c2 := collection.New([]string{"apple", "banana", "cherry"})
var out []string
c2.Each(func(s string) {
out = append(out, strings.ToUpper(s))
})
collection.Dump(out)
// #[]string [
// 0 => "APPLE" #string
// 1 => "BANANA" #string
// 2 => "CHERRY" #string
// ]Example: structs
type User struct {
ID int
Name string
}
users := collection.New([]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
{ID: 3, Name: "Charlie"},
})
var names []string
users.Each(func(u User) {
names = append(names, u.Name)
})
collection.Dump(names)
// #[]string [
// 0 => "Alice" #string
// 1 => "Bob" #string
// 2 => "Charlie" #string
// ]Map · mutable · chainable
Map applies a same-type transformation in place and returns the same collection.
Example: integers
c := collection.New([]int{1, 2, 3})
mapped := c.Map(func(v int) int {
return v * 10
})
collection.Dump(mapped.Items())
// #[]int [
// 0 => 10 #int
// 1 => 20 #int
// 2 => 30 #int
// ]Example: strings
c2 := collection.New([]string{"apple", "banana", "cherry"})
upper := c2.Map(func(s string) string {
return strings.ToUpper(s)
})
collection.Dump(upper.Items())
// #[]string [
// 0 => "APPLE" #string
// 1 => "BANANA" #string
// 2 => "CHERRY" #string
// ]Example: structs
type User struct {
ID int
Name string
}
users := collection.New([]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
})
updated := users.Map(func(u User) User {
u.Name = strings.ToUpper(u.Name)
return u
})
collection.Dump(updated.Items())
// #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// +Name => "ALICE" #string
// }
// 1 => #main.User {
// +ID => 2 #int
// +Name => "BOB" #string
// }
// ]MapTo · immutable · chainable
MapTo maps a Collection[T] to a Collection[R] using fn(T) R.
Example: integers - extract parity label
nums := collection.New([]int{1, 2, 3, 4})
parity := collection.MapTo(nums, func(n int) string {
if n%2 == 0 {
return "even"
}
return "odd"
})
collection.Dump(parity.Items())
// #[]string [
// 0 => "odd" #string
// 1 => "even" #string
// 2 => "odd" #string
// 3 => "even" #string
// ]Example: strings - length of each value
words := collection.New([]string{"go", "forj", "rocks"})
lengths := collection.MapTo(words, func(s string) int {
return len(s)
})
collection.Dump(lengths.Items())
// #[]int [
// 0 => 2 #int
// 1 => 4 #int
// 2 => 5 #int
// ]Example: structs - MapTo a field
type User struct {
ID int
Name string
}
users := collection.New([]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
})
names := collection.MapTo(users, func(u User) string {
return u.Name
})
collection.Dump(names.Items())
// #[]string [
// 0 => "Alice" #string
// 1 => "Bob" #string
// ]Merge · immutable · chainable
Merge merges the given data into a new collection.
Example: integers - merging slices
ints := collection.New([]int{1, 2})
extra := []int{3, 4}
// Merge the extra slice into the ints collection
merged1 := ints.Merge(extra)
collection.Dump(merged1.Items())
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// 3 => 4 #int
// ]Example: strings - merging another collection
strs := collection.New([]string{"a", "b"})
more := collection.New([]string{"c", "d"})
merged2 := strs.Merge(more)
collection.Dump(merged2.Items())
// #[]string [
// 0 => "a" #string
// 1 => "b" #string
// 2 => "c" #string
// 3 => "d" #string
// ]Example: structs - merging struct slices
type User struct {
ID int
Name string
}
users := collection.New([]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
})
moreUsers := []User{
{ID: 3, Name: "Carol"},
{ID: 4, Name: "Dave"},
}
merged3 := users.Merge(moreUsers)
collection.Dump(merged3.Items())
// #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// +Name => "Alice" #string
// }
// 1 => #main.User {
// +ID => 2 #int
// +Name => "Bob" #string
// }
// 2 => #main.User {
// +ID => 3 #int
// +Name => "Carol" #string
// }
// 3 => #main.User {
// +ID => 4 #int
// +Name => "Dave" #string
// }
// ]Multiply · immutable · chainable
Multiply creates n copies of all items in the collection and returns a new collection.
Example: integers
ints := collection.New([]int{1, 2})
out := ints.Multiply(3)
collection.Dump(out.Items())
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 1 #int
// 3 => 2 #int
// 4 => 1 #int
// 5 => 2 #int
// ]Example: strings
strs := collection.New([]string{"a", "b"})
out2 := strs.Multiply(2)
collection.Dump(out2.Items())
// #[]string [
// 0 => "a" #string
// 1 => "b" #string
// 2 => "a" #string
// 3 => "b" #string
// ]Example: structs
type User struct {
Name string
}
users := collection.New([]User{{Name: "Alice"}, {Name: "Bob"}})
out3 := users.Multiply(2)
collection.Dump(out3.Items())
// #[]main.User [
// 0 => #main.User {
// +Name => "Alice" #string
// }
// 1 => #main.User {
// +Name => "Bob" #string
// }
// 2 => #main.User {
// +Name => "Alice" #string
// }
// 3 => #main.User {
// +Name => "Bob" #string
// }
// ]Example: multiplying by zero or negative returns empty
none := ints.Multiply(0)
collection.Dump(none.Items())
// #[]int [
// ]Pipe · readonly · terminal
Pipe passes the entire collection into the given function and returns the function's result.
Example: integers – computing a sum
c := collection.New([]int{1, 2, 3})
sum := collection.Pipe(c, func(col *collection.Collection[int]) int {
total := 0
for _, v := range col.Items() {
total += v
}
return total
})
collection.Dump(sum)
// 6 #intExample: strings – joining values
c2 := collection.New([]string{"a", "b", "c"})
joined := collection.Pipe(c2, func(col *collection.Collection[string]) string {
out := ""
for _, v := range col.Items() {
out += v
}
return out
})
collection.Dump(joined)
// "abc" #stringExample: structs – extracting just the names
type User struct {
ID int
Name string
}
users := collection.New([]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
})
names := collection.Pipe(users, func(col *collection.Collection[User]) []string {
result := make([]string, 0, len(col.Items()))
for _, u := range col.Items() {
result = append(result, u.Name)
}
return result
})
collection.Dump(names)
// #[]string [
// 0 => "Alice" #string
// 1 => "Bob" #string
// ]Prepend · mutable · chainable
Prepend adds the given values to the beginning of the collection.
Example: integers
c := collection.New([]int{3, 4})
c.Prepend(1, 2)
collection.Dump(c.Items())
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// 3 => 4 #int
// ]Example: strings
letters := collection.New([]string{"c", "d"})
letters.Prepend("a", "b")
collection.Dump(letters.Items())
// #[]string [
// 0 => "a" #string
// 1 => "b" #string
// 2 => "c" #string
// 3 => "d" #string
// ]Example: structs
type User struct {
ID int
Name string
}
users := collection.New([]User{
{ID: 2, Name: "Bob"},
})
users.Prepend(User{ID: 1, Name: "Alice"})
collection.Dump(users.Items())
// #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// +Name => "Alice" #string
// }
// 1 => #main.User {
// +ID => 2 #int
// +Name => "Bob" #string
// }
// ]Example: integers - Prepending into an empty collection
empty := collection.New([]int{})
empty.Prepend(9, 8)
collection.Dump(empty.Items())
// #[]int [
// 0 => 9 #int
// 1 => 8 #int
// ]Example: integers - Prepending no values → no change
c2 := collection.New([]int{1, 2})
c2.Prepend()
collection.Dump(c2.Items())
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// ]Tap · immutable · chainable
Tap invokes fn with the collection pointer for side effects (logging, debugging, inspection) and returns the same collection to allow chaining.
Example: integers - capture intermediate state during a chain
captured1 := []int{}
c1 := collection.New([]int{3, 1, 2}).
Sort(func(a, b int) bool { return a < b }). // → [1, 2, 3]
Tap(func(col *collection.Collection[int]) {
captured1 = append([]int(nil), col.Items()...) // snapshot copy
}).
Filter(func(v int) bool { return v >= 2 }).
Dump()
// #[]int [
// 0 => 2 #int
// 1 => 3 #int
// ]
// Use BOTH variables so nothing is "declared and not used"
collection.Dump(c1.Items())
collection.Dump(captured1)
// #[]int [
// 0 => 2 #int
// 1 => 3 #int
// ]
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// ]Example: integers - tap for debugging without changing flow
c2 := collection.New([]int{10, 20, 30}).
Tap(func(col *collection.Collection[int]) {
collection.Dump(col.Items())
// #[]int [
// 0 => 10 #int
// 1 => 20 #int
// 2 => 30 #int
// ]
}).
Filter(func(v int) bool { return v > 10 })
collection.Dump(c2.Items()) // ensures c2 is used
// #[]int [
// 0 => 20 #int
// 1 => 30 #int
// ]Example: structs - Tap with struct collection
type User struct {
ID int
Name string
}
users := collection.New([]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
})
users2 := users.Tap(func(col *collection.Collection[User]) {
collection.Dump(col.Items())
// #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// +Name => "Alice" #string
// }
// 1 => #main.User {
// +ID => 2 #int
// +Name => "Bob" #string
// }
// ]
})
collection.Dump(users2.Items()) // ensures users2 is used
// #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// +Name => "Alice" #string
// }
// 1 => #main.User {
// +ID => 2 #int
// +Name => "Bob" #string
// }
// ]Times · immutable · chainable
Times creates a new collection by calling fn(i) for i = 1..count. This mirrors Laravel's Collection::times(), which is 1-indexed.
Example: integers - double each index
cTimes1 := collection.Times(5, func(i int) int {
return i * 2
})
collection.Dump(cTimes1.Items())
// #[]int [
// 0 => 2 #int
// 1 => 4 #int
// 2 => 6 #int
// 3 => 8 #int
// 4 => 10 #int
// ]Example: strings
cTimes2 := collection.Times(3, func(i int) string {
return fmt.Sprintf("item-%d", i)
})
collection.Dump(cTimes2.Items())
// #[]string [
// 0 => "item-1" #string
// 1 => "item-2" #string
// 2 => "item-3" #string
// ]Example: structs
type Point struct {
X int
Y int
}
cTimes3 := collection.Times(4, func(i int) Point {
return Point{X: i, Y: i * i}
})
collection.Dump(cTimes3.Items())
// #[]main.Point [
// 0 => #main.Point {
// +X => 1 #int
// +Y => 1 #int
// }
// 1 => #main.Point {
// +X => 2 #int
// +Y => 4 #int
// }
// 2 => #main.Point {
// +X => 3 #int
// +Y => 9 #int
// }
// 3 => #main.Point {
// +X => 4 #int
// +Y => 16 #int
// }
// ]Transform · mutable · terminal
Transform applies fn to every item in place, mutating the collection.
Example: integers
c1 := collection.New([]int{1, 2, 3})
c1.Transform(func(v int) int { return v * 2 })
collection.Dump(c1.Items())
// #[]int [
// 0 => 2 #int
// 1 => 4 #int
// 2 => 6 #int
// ]Example: strings
c2 := collection.New([]string{"a", "b", "c"})
c2.Transform(func(s string) string { return strings.ToUpper(s) })
collection.Dump(c2.Items())
// #[]string [
// 0 => "A" #string
// 1 => "B" #string
// 2 => "C" #string
// ]Example: structs
type User struct {
ID int
Name string
}
c3 := collection.New([]User{
{ID: 1, Name: "alice"},
{ID: 2, Name: "bob"},
})
c3.Transform(func(u User) User {
u.Name = strings.ToUpper(u.Name)
return u
})
collection.Dump(c3.Items())
// #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// +Name => "ALICE" #string
// }
// 1 => #main.User {
// +ID => 2 #int
// +Name => "BOB" #string
// }
// ]Zip · immutable · chainable
Zip combines two collections element-wise into a collection of tuples. The resulting length is the smaller of the two inputs.
Example: integers and strings
nums := collection.New([]int{1, 2, 3})
words := collection.New([]string{"one", "two"})
out := collection.Zip(nums, words)
collection.Dump(out.Items())
// #[]collection.Tuple[int,string] [
// 0 => #collection.Tuple[int,string] {
// +First => 1 #int
// +Second => "one" #string
// }
// 1 => #collection.Tuple[int,string] {
// +First => 2 #int
// +Second => "two" #string
// }
// ]Example: structs
type User struct {
ID int
Name string
}
users := collection.New([]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
})
roles := collection.New([]string{"admin", "user", "extra"})
out2 := collection.Zip(users, roles)
collection.Dump(out2.Items())
// #[]collection.Tuple[main.User·1,string] [
// 0 => #collection.Tuple[main.User·1,string] {
// +First => #main.User {
// +ID => 1 #int
// +Name => "Alice" #string
// }
// +Second => "admin" #string
// }
// 1 => #collection.Tuple[main.User·1,string] {
// +First => #main.User {
// +ID => 2 #int
// +Name => "Bob" #string
// }
// +Second => "user" #string
// }
// ]ZipWith · immutable · chainable
ZipWith combines two collections element-wise using combiner fn. The resulting length is the smaller of the two inputs.
Example: sum ints
a := collection.New([]int{1, 2, 3})
b := collection.New([]int{10, 20})
out := collection.ZipWith(a, b, func(x, y int) int {
return x + y
})
collection.Dump(out.Items())
// #[]int [
// 0 => 11 #int
// 1 => 22 #int
// ]Example: format strings
names := collection.New([]string{"alice", "bob"})
roles := collection.New([]string{"admin", "user", "extra"})
out2 := collection.ZipWith(names, roles, func(name, role string) string {
return name + ":" + role
})
collection.Dump(out2.Items())
// #[]string [
// 0 => "alice:admin" #string
// 1 => "bob:user" #string
// ]Example: structs
type User struct {
Name string
}
type Role struct {
Title string
}
users := collection.New([]User{{Name: "Alice"}, {Name: "Bob"}})
roles2 := collection.New([]Role{{Title: "admin"}})
out3 := collection.ZipWith(users, roles2, func(u User, r Role) string {
return u.Name + " -> " + r.Title
})
collection.Dump(out3.Items())
// #[]string [
// 0 => "Alice -> admin" #string
// ]