Fluent, Laravel-style Collections for Go - with generics, chainable pipelines, and expressive data transforms.
collection brings an expressive, fluent API to Go.
Iterate, filter, transform, sort, reduce, group, and debug your data with zero dependencies.
Designed to feel natural to Go developers - and luxurious to everyone else.
- Fluent chaining - pipeline your operations like Laravel Collections
- Fully generic (
Collection[T]) - no reflection, nointerface{} - Zero dependencies - pure Go, fast, lightweight
- Minimal allocations - avoids unnecessary copies; most operations reuse the underlying slice
- Map / Filter / Reduce - clean functional transforms
- First / Last / Find / Contains helpers
- Sort, GroupBy, Chunk, and more
- Safe-by-default - defensive copies where appropriate
- 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
Many methods return the collection itself, allowing for fluent method chaining.
Some methods maybe limited to 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
// }
// ]lo is a fantastic library and a major inspiration for this project. Our focus differs: collection is built for fluent chaining with explicit mutability, which lets hot paths avoid intermediate allocations. That shows up most in chained pipelines and in-place operations where we can keep work on the same backing slice while still being explicit about behavior.
| Op | ns/op (vs lo) | × | bytes/op (vs lo) | × | allocs/op (vs lo) |
|---|---|---|---|---|---|
| All | 232ns / 230ns | ≈ | 0B / 0B | ≈ | 0 / 0 |
| Any | 232ns / 234ns | ≈ | 0B / 0B | ≈ | 0 / 0 |
| Chunk | 128ns / 1.1µs | 8.30x | 1.3KB / 9.3KB | 7.25x less | 1 / 51 |
| Contains | 238ns / 250ns | 1.05x | 0B / 0B | ≈ | 0 / 0 |
| CountBy | 8.1µs / 8.2µs | ≈ | 9.4KB / 9.4KB | ≈ | 11 / 11 |
| CountByValue | 8.1µs / 8.1µs | ≈ | 9.4KB / 9.4KB | ≈ | 11 / 11 |
| Difference | 19.4µs / 44.5µs | 2.29x | 82.1KB / 108.8KB | 1.33x less | 12 / 43 |
| Each | 235ns / 230ns | ≈ | 0B / 0B | ≈ | 0 / 0 |
| Filter | 647ns / 1.1µs | 1.67x | 0B / 8.2KB | ∞x less | 0 / 1 |
| Find | 239ns / 235ns | ≈ | 0B / 0B | ≈ | 0 / 0 |
| First | 0ns / 0ns | ∞ | 0B / 0B | ≈ | 0 / 0 |
| GroupBySlice | 8.2µs / 8.3µs | ≈ | 21.0KB / 21.0KB | ≈ | 83 / 83 |
| IndexWhere | 232ns / 231ns | ≈ | 0B / 0B | ≈ | 0 / 0 |
| Intersect | 11.0µs / 10.8µs | ≈ | 11.4KB / 11.4KB | ≈ | 19 / 19 |
| Last | 0ns / 0ns | ∞ | 0B / 0B | ≈ | 0 / 0 |
| Map | 347ns / 821ns | 2.37x | 0B / 8.2KB | ∞x less | 0 / 1 |
| Max | 230ns / 231ns | ≈ | 0B / 0B | ≈ | 0 / 0 |
| Min | 232ns / 229ns | ≈ | 0B / 0B | ≈ | 0 / 0 |
| None | 232ns / 232ns | ≈ | 0B / 0B | ≈ | 0 / 0 |
| Pipeline F→M→T→R | 496ns / 1.3µs | 2.62x | 0B / 12.3KB | ∞x less | 0 / 2 |
| Reduce (sum) | 230ns / 231ns | ≈ | 0B / 0B | ≈ | 0 / 0 |
| Reverse | 216ns / 230ns | 1.06x | 0B / 0B | ≈ | 0 / 0 |
| Shuffle | 3.6µs / 5.3µs | 1.49x | 0B / 0B | ≈ | 0 / 0 |
| Skip | 0ns / 721ns | ∞ | 0B / 8.2KB | ∞x less | 0 / 1 |
| SkipLast | 0ns / 730ns | ∞ | 0B / 8.2KB | ∞x less | 0 / 1 |
| Sum | 232ns / 233ns | ≈ | 0B / 0B | ≈ | 0 / 0 |
| Take | 0ns / 0ns | ∞ | 0B / 0B | ≈ | 0 / 0 |
| ToMap | 7.7µs / 7.8µs | ≈ | 36.9KB / 37.0KB | ≈ | 5 / 6 |
| Union | 17.4µs / 17.7µs | ≈ | 90.3KB / 90.3KB | ≈ | 11 / 10 |
| Unique | 6.4µs / 6.5µs | ≈ | 45.1KB / 45.1KB | ≈ | 6 / 6 |
| UniqueBy | 6.9µs / 6.7µs | ≈ | 45.2KB / 45.1KB | ≈ | 7 / 6 |
| Zip | 1.4µs / 3.2µs | 2.27x | 16.4KB / 16.4KB | ≈ | 1 / 1 |
| ZipWith | 1.0µs / 3.1µs | 3.07x | 8.2KB / 8.2KB | ≈ | 1 / 1 |
- ≈ means the two libraries are effectively equivalent
- ∞x less means one side allocates while the other allocates nothing
- 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.
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.
Most functional helpers (including lo) operate like this:
input → Map → new slice → Filter → new slice → Take → new slice
That model is simple and safe - but each step typically allocates.
collection pipelines are designed to look more like this:
input → Filter (in place) → Sort (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.
Fluent pipelines don't mean you're locked into mutation.
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.
- Type-safe: no reflection, no
anyleaks - 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
- Not a lazy or streaming library
- Not concurrency-aware
- Not immutable-by-default
- Not a replacement for idiomatic loops in simple cases
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.
Each method declares how it interacts with the collection:
- readonly – reads data only, returns a derived value
- immutable – returns a new collection, original unchanged
- mutable – modifies the collection in place
Annotations describe observable behavior, not implementation details.
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.
go get github.com/goforj/collection| Group | Functions |
|---|---|
| Access | Items |
| 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 FindWhere 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 Pluck Prepend Push Tap Times Transform Zip ZipWith |
Items returns the underlying 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
// }
// ]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 returns the total number of items in the collection.
count := collection.New([]int{1, 2, 3, 4}).Count()
collection.Dump(count)
// 4 #intCountBy 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 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[collection.Point]int [
// {X:1 Y:1} => 2 #int
// {X:2 Y:2} => 1 #int
// ]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 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 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 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 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 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)
// <nil>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 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 #intClone returns a shallow copy of the collection.
Example: basic cloning
c := collection.New([]int{1, 2, 3})
clone := c.Clone()
clone.Push(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 creates a new Collection from the provided slice.
NewNumeric wraps a slice of numeric types in a NumericCollection. A shallow copy is made so that further operations don't mutate the original slice.
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 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 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
// ]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 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
// }
// ]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)
collection.Dump(c.Items())
// #[]collection.Pair[string,int] [
// 0 => {Key:"a" Value:1}
// 1 => {Key:"b" Value:2}
// 2 => {Key:"c" Value:3}
// ]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
}).
Items()
collection.Dump(out)
// #[]collection.Pair[string,collection.Config] [
// 0 => {Key:"router-1" Value:{Enabled:true Timeout:30}}
// 1 => {Key:"router-3" Value:{Enabled:true Timeout:45}}
// ]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
// "bob" => 2
// ]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)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)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
// "b" => 2
// "c" => 3
// ]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]collection.Config [
// "router-1" => {Enabled:true Timeout:30}
// "router-3" => {Enabled:true Timeout:45}
// ]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 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())
// #[]collection.User [
// 0 => {Name:"Alice" Admin:false} #collection.User
// 1 => {Name:"Bob" Admin:false} #collection.User
// ]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())
// #[]collection.User [
// 0 => {ID:3} #collection.User
// 1 => {ID:2} #collection.User
// 2 => {ID:1} #collection.User
// ]Shuffle randomly shuffles the items in the collection in place and returns the same collection for chaining.
Example: integers
c := collection.New([]int{1, 2, 3, 4, 5})
c.Shuffle()
collection.Dump(c.Items())Example: strings – chaining
out := collection.New([]string{"a", "b", "c"}).
Shuffle().
Append("d").
Items()
collection.Dump(out)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 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
// }
// ]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 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 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 trueExample: out of bounds
v2, ok2 := c.At(10)
collection.Dump(v2, ok2)
// 0 falseExample: 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)
// {ID:1 Name:"Alice"} trueContains returns true if any item satisfies the predicate.
Example: integers
c := collection.New([]int{1, 2, 3, 4, 5})
hasEven := c.Contains(func(v int) bool {
return v%2 == 0
})
collection.Dump(hasEven)
// true #boolExample: strings
c2 := collection.New([]string{"apple", "banana", "cherry"})
hasBanana := c2.Contains(func(v string) bool {
return v == "banana"
})
collection.Dump(hasBanana)
// 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: "Carol"},
})
hasBob := users.Contains(func(u User) bool {
return u.Name == "Bob"
})
collection.Dump(hasBob)
// true #boolFindWhere returns the first item in the collection for which the provided predicate function returns true. This is an alias for FirstWhere(fn) and exists for ergonomic parity with functional languages (JavaScript, Rust, C#, Python) where developers expect a “find” helper.
Example: integers
nums := collection.New([]int{1, 2, 3, 4, 5})
v1, ok1 := nums.FindWhere(func(n int) bool {
return n == 3
})
collection.Dump(v1, ok1)
// 3 #int
// true #boolExample: no match
v2, ok2 := nums.FindWhere(func(n int) bool {
return n > 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"},
{ID: 3, Name: "Charlie"},
})
u, ok3 := users.FindWhere(func(u User) bool {
return u.ID == 2
})
collection.Dump(u, ok3)
// #collection.User {
// +ID => 2 #int
// +Name => "Bob" #string
// }
// true #boolExample: integers - empty collection
empty := collection.New([]int{})
v4, ok4 := empty.FindWhere(func(n int) bool { return n == 1 })
collection.Dump(v4, ok4)
// 0 #int
// false #boolFirst 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 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 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 trueExample: not found
idx2, ok2 := c.IndexWhere(func(v int) bool { return v == 99 })
collection.Dump(idx2, ok2)
// 0 falseExample: 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 trueIsEmpty 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 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 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 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 #boolToJSON 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 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"
// ]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 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 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 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 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())
// #[]collection.User [
// 0 => {ID:1 Name:"Alice"} #collection.User
// 1 => {ID:2 Name:"Bob"} #collection.User
// ]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())
// #[]collection.User [
// 0 => {ID:1 Name:"Alice"} #collection.User
// 1 => {ID:2 Name:"Bob"} #collection.User
// ]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 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
// ]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 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 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 returns the last item and a new collection with that item removed. The original collection remains unchanged.
Example: integers
c := collection.New([]int{1, 2, 3})
item, rest := c.Pop()
collection.Dump(item, rest.Items())
// 3 #int
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// ]Example: strings
c2 := collection.New([]string{"a", "b", "c"})
item2, rest2 := c2.Pop()
collection.Dump(item2, rest2.Items())
// "c" #string
// #[]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, rest3 := users.Pop()
collection.Dump(item3, rest3.Items())
// #main.User {
// +ID => 2 #int
// +Name => "Bob" #string
// }
// #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// +Name => "Alice" #string
// }
// ]Example: empty collection
empty := collection.New([]int{})
item4, rest4 := empty.Pop()
collection.Dump(item4, rest4.Items())
// 0 #int
// #[]int [
// ]PopN removes and returns the last n items as a new collection, and returns a second collection containing the remaining items.
Example: integers – pop 2
c := collection.New([]int{1, 2, 3, 4})
popped, rest := c.PopN(2)
collection.Dump(popped.Items(), rest.Items())
// #[]int [
// 0 => 4 #int
// 1 => 3 #int
// ]
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// ]Example: strings – pop 1
c2 := collection.New([]string{"a", "b", "c"})
popped2, rest2 := c2.PopN(1)
collection.Dump(popped2.Items(), rest2.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, rest3 := users.PopN(2)
collection.Dump(popped3.Items(), rest3.Items())
// #[]main.User [
// 0 => #main.User {
// +ID => 3 #int
// +Name => "Carol" #string
// }
// 1 => #main.User {
// +ID => 2 #int
// +Name => "Bob" #string
// }
// ]
// #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// +Name => "Alice" #string
// }
// ]Example: integers - n <= 0 → returns empty popped + original collection
c3 := collection.New([]int{1, 2, 3})
popped4, rest4 := c3.PopN(0)
collection.Dump(popped4.Items(), rest4.Items())
// #[]int [
// ]
// #[]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, rest5 := c4.PopN(10)
collection.Dump(popped5.Items(), rest5.Items())
// #[]string [
// 0 => "y" #string
// 1 => "x" #string
// ]
// #[]string [
// ]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 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())
// #[]collection.User [
// 0 => {ID:1} #collection.User
// 1 => {ID:2} #collection.User
// ]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 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())
// #[]collection.User [
// 0 => {ID:3} #collection.User
// ]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 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 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
// }
// ]
// ]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 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 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 applies a same-type transformation and returns a new 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 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 merges the given data into the current 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 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 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 := c.Pipe(func(col *collection.Collection[int]) any {
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 := c2.Pipe(func(col *collection.Collection[string]) any {
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 := users.Pipe(func(col *collection.Collection[User]) any {
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
// ]Pluck is an alias for MapTo with a more semantic name when projecting fields. It extracts a single field or computed value from every element and returns a new typed collection.
Example: integers - extract parity label
nums := collection.New([]int{1, 2, 3, 4})
parity := collection.Pluck(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.Pluck(words, func(s string) int {
return len(s)
})
collection.Dump(lengths.Items())
// #[]int [
// 0 => 2 #int
// 1 => 4 #int
// 2 => 5 #int
// ]Example: structs - pluck a field
type User struct {
ID int
Name string
}
users := collection.New([]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
})
names := collection.Pluck(users, func(u User) string {
return u.Name
})
collection.Dump(names.Items())
// #[]string [
// 0 => "Alice" #string
// 1 => "Bob" #string
// ]Prepend returns a new collection with the given values added to the beginning of the collection.
Example: integers
c := collection.New([]int{3, 4})
newC := c.Prepend(1, 2)
collection.Dump(newC.Items())
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// 3 => 4 #int
// ]Example: strings
letters := collection.New([]string{"c", "d"})
out := letters.Prepend("a", "b")
collection.Dump(out.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"},
})
out2 := users.Prepend(User{ID: 1, Name: "Alice"})
collection.Dump(out2.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{})
out3 := empty.Prepend(9, 8)
collection.Dump(out3.Items())
// #[]int [
// 0 => 9 #int
// 1 => 8 #int
// ]Example: integers - Prepending no values → returns a copy of original
c2 := collection.New([]int{1, 2})
out4 := c2.Prepend()
collection.Dump(out4.Items())
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// ]Push returns a new collection with the given values appended.
nums := collection.New([]int{1, 2}).Push(3, 4)
nums.Dump()
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// 3 => 4 #int
// ]
// Complex type (structs)
type User struct {
Name string
Age int
}
users := collection.New([]User{
{Name: "Alice", Age: 30},
{Name: "Bob", Age: 25},
}).Push(
User{Name: "Carol", Age: 40},
User{Name: "Dave", Age: 20},
)
users.Dump()
// #[]main.User [
// 0 => #main.User {
// +Name => "Alice" #string
// +Age => 30 #int
// }
// 1 => #main.User {
// +Name => "Bob" #string
// +Age => 25 #int
// }
// 2 => #main.User {
// +Name => "Carol" #string
// +Age => 40 #int
// }
// 3 => #main.User {
// +Name => "Dave" #string
// +Age => 20 #int
// }
// ]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)
// c1 → #[]int [2,3]
// captured1 → #[]int [1,2,3]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())
}).
Filter(func(v int) bool { return v > 10 })
collection.Dump(c2.Items()) // ensures c2 is usedExample: 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())
})
collection.Dump(users2.Items()) // ensures users2 is usedTimes 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 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())
// #[]collection.User [
// 0 => {ID:1 Name:"ALICE"} #collection.User
// 1 => {ID:2 Name:"BOB"} #collection.User
// ]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,string] [
// 0 => #collection.Tuple[main.User,string] {
// +First => #main.User {
// +ID => 1 #int
// +Name => "Alice" #string
// }
// +Second => "admin" #string
// }
// 1 => #collection.Tuple[main.User,string] {
// +First => #main.User {
// +ID => 2 #int
// +Name => "Bob" #string
// }
// +Second => "user" #string
// }
// ]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
// ]