Fluent collections for Go. Iterate, filter, transform, sort, reduce, group, and debug data with a tiny dependency footprint.
- Fluent chaining - pipeline your operations like Laravel Collections
- Fully generic (
Slice[T]) - the public collection API uses typed generic signatures; debugging helpers accept arbitrary values for inspection - Tiny dependency footprint - only
godumpfor debugging helpers - Go-native slice access - use
len, indexing,range, and ordinary slice conversions directly - Explicit ownership behavior - slice views, independent results, and in-place mutations are documented per operation
- Map / Filter / Reduce - clean functional transforms
- Generic methods - type-changing transforms remain fluent on Go 1.27+
- First / Last / FirstWhere / IndexWhere helpers
- Sort, GroupBy, Chunk, and more
- Borrow-by-default - no defensive copies unless you ask for them
- Standard-library interop - use
slices,iter, andencoding/jsondirectly - Developer-friendly debug helpers (
Dump(),Dd(),DumpStr()) - Works with any Go type, including structs, pointers, and deeply nested composites
Many methods return a Slice, allowing fluent method chaining without a wrapper object.
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},
{Device: "router-4", Region: "us-west", Errors: 9},
{Device: "router-5", Region: "eu-west", Errors: 7},
}
// Clone creates a top-level ownership boundary before the mutable stages.
collection.
New(events). // Construction
Clone(). // Construction
Retain(func(e DeviceEvent) bool { return e.Errors > 5 }). // Mutation
Sort(func(a, b DeviceEvent) bool { return a.Errors > b.Errors }). // Ordering
Take(3). // Slicing
TakeUntil(func(e DeviceEvent) bool { return e.Errors <= 9 }). // Slicing (stop when predicate becomes true)
Reverse(). // Ordering
Dump() // Debugging
// #[]main.DeviceEvent [
// 0 => #main.DeviceEvent {
// +Device => "router-2" #string
// +Region => "us-east" #string
// +Errors => 15 #int
// }
// 1 => #main.DeviceEvent {
// +Device => "router-3" #string
// +Region => "us-west" #string
// +Errors => 22 #int
// }
// ]Go 1.27 generic methods keep type-changing pipelines fluent:
regionsByPrefix := collection.New(events).
Map(func(event DeviceEvent) string { return event.Region }).
UniqueBy(func(region string) string { return region }).
GroupBy(func(region string) string { return region[:2] })
fmt.Println(len(regionsByPrefix))
// 2Equivalent operations are benchmarked against lo, with ownership and API differences labeled separately. Both the summary and full results use collection.New, which borrows its input.
Benchmark tables and methodology
lo is a major inspiration for this project.
Both libraries provide generic operations over ordinary slices. The main difference is API shape: collection.Slice adds receiver methods for eager fluent chains, while lo primarily uses free functions and provides mutable and iterator variants in separate packages. The tables label allocation, view, mutation, API, and different-work cases rather than treating them as equivalent.
The below tables are automatically generated from ./docs/bench/main.go.
Matched v2/v4 regression benchmarks for mutating, copied, and pipeline workloads live in ./docs/regression; its go.mod records the exact v2 baseline.
Full raw tables: see BENCHMARKS.md.
| Op | Speed vs lo | Memory | Allocs |
|---|---|---|---|
| All | ≈ | ≈ | ≈ |
| Any | ≈ | ≈ | ≈ |
| None | ≈ | ≈ | ≈ |
| First | below floor | ≈ | ≈ |
| Last | below floor | ≈ | ≈ |
| FirstWhere | same loop | ≈ | ≈ |
| IndexWhere | ≈ | ≈ | ≈ |
| slices.Contains | ≈ | ≈ | ≈ |
| Reduce (sum) | ≈ | ≈ | ≈ |
| Sum | ≈ | ≈ | ≈ |
| Min | ≈ | ≈ | ≈ |
| Max | ≈ | ≈ | ≈ |
| Each | ≈ | ≈ | ≈ |
| Op | Speed vs lo | Memory | Allocs |
|---|---|---|---|
| Chunk | view trade-off | ownership trade-off | ownership trade-off |
| Filter | ≈ | ≈ | ≈ |
| Map | ≈ | ≈ | ≈ |
| Take | below floor | ≈ | ≈ |
| Skip | view trade-off | ownership trade-off | ownership trade-off |
| SkipLast | view trade-off | ownership trade-off | ownership trade-off |
| Zip | inconclusive | ≈ | ≈ |
| ZipWith | 2.9x faster | ≈ | ≈ |
| UniqueComparable | ≈ | ≈ | ≈ |
| UniqueBy | ≈ | ≈ | ≈ |
| Union | inconclusive | ≈ | ≈ |
| Intersect | ≈ | ≈ | ≈ |
| Difference | different work | API trade-off | API trade-off |
| GroupBy | ≈ | ≈ | ≈ |
| CountBy | ≈ | ≈ | ≈ |
| CountByValue | ≈ | ≈ | ≈ |
| ToMap | ≈ | ≈ | ≈ |
| Op | Speed vs lo | Memory | Allocs |
|---|---|---|---|
| Pipeline F→M→T→R | ≈ | ≈ | ≈ |
| Op | Speed vs lo | Memory | Allocs |
|---|---|---|---|
| Retain | 1.6x slower | ≈ | ≈ |
| Reverse | 1.1x faster | ≈ | ≈ |
| Shuffle | 3.7x faster | ≈ | ≈ |
| Transform | ≈ | ≈ | ≈ |
- In Speed/Timing, ≈ means the median is inside ±10% (±15% in the condensed read-only scalar table)
- below floor means both timings are under 50 ns, so no relative conclusion is drawn
- inconclusive means the median is outside that band but paired samples did not consistently establish the difference
- Nx faster/slower is calculated from the measured, unrounded medians and appears only when all paired samples establish the same direction; exact values remain machine- and build-specific
- In Memory/Allocs, ≈ means both implementations produced the same measured result
- same loop means both implementations compile to the same machine loop, so binary-placement skew is not presented as a library difference
- Explicit memory deltas show allocation differences for equivalent work; ownership and API trade-offs are labeled separately
- Single-operation helpers are expected to be close when they perform equivalent work
- Multi-step pipelines show the cost of the selected ownership model
Version 3 has one Go-native, slice-backed representation. Every Slice supports
len, indexing, slicing, and range; each operation documents how it treats the
backing array.
Map,Filter,Concat, andPrependreturn independent resultsRetainandTransformmutate in place; the shortened result fromRetainmust be capturedSort,Reverse, andShufflemutate elements in place- View-producing slicing operations return capacity-capped views
Clonecreates an intentional ownership boundary before mutation
The benchmark tables compare equivalent pure operations separately from
Retain and Transform. Chunk, Skip, and SkipLast are ownership
trade-offs because collection returns capacity-capped views while lo returns
copied slices.
Fluent pipelines don't mean you're locked into mutation.
New borrows slices by default. Use Clone() before an in-place operation when
the original slice's element slots or order must remain unchanged.
When you want to branch a pipeline or preserve the original data, Clone() creates a shallow copy of the top-level slice. Subsequent operations that replace, reorder, or overwrite collection entries are isolated; Clone() does not deep-copy element values or values they reference.
events := collection.New(deviceEvents)
// In-place alert filtering with a bounded result
alerts := events.
Clone().
Retain(func(e DeviceEvent) bool { return e.Severity >= Critical }).
Take(10)
// Deeper analysis path: heavier work, full ordering
report := events.
Filter(func(e DeviceEvent) bool { return e.Region == "us-east" }).
Sort(func(a, b DeviceEvent) bool { return a.Timestamp.Before(b.Timestamp) })This makes divergence points explicit and intentional.
Copying and mutation behavior is documented per operation.
- Type-safe: the collection API uses typed generic signatures; debugging helpers are the exception for arbitrary-value inspection
- 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
- Not designed to hide allocation, mutation, or ordering semantics
Maps are unordered in Go. This library does not pretend otherwise.
Instead, map interaction is explicit and intentional:
FromMapmaterializes key/value pairs into a collection; its initial order is unspecified, so sort when a particular order mattersToMapreduces collections back into maps explicitly
This makes map materialization visible and leaves deterministic ordering to an explicit Sort.
Each exported function and method declares how it interacts with the collection:
- readonly - does not directly mutate the receiver's backing array; callbacks and debugging helpers can still have side effects
- immutable - returns a value without mutating the receiver; individual method docs state whether it allocates or returns a view
- mutable - may modify elements in the receiver's backing array
- 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 Slice and cannot be chained further.
They are designed to be allocation-free under New() where possible.
Ownership and copying are explicitly documented per operation. Some readonly or immutable operations may allocate internally when required (e.g. grouping, chunking, scratch copies), but never mutate the receiver.
Borrowed slices, independent results, in-place element mutation, and view semantics are intentional and visible.
Slice[T] is a named slice, so ordinary Go operations work directly:
values := collection.New([]int{10, 20, 30})
fmt.Println(len(values))
// 3
fmt.Println(values[1])
// 20
for _, value := range values {
fmt.Println(value)
}
// 10
// 20
// 30Use slices.Values(values) or slices.All(values) when an iterator is useful;
no collection-specific lazy wrapper is required.
Every exported function and method has a corresponding runnable example under ./examples.
The checked-in examples are generated from GoDoc comments when the documentation is refreshed; they are intended to stay aligned with the README and GoDoc.
Automated checks build and execute the checked-in examples, then compare their output with the documentation.
This helps catch example regressions as the API evolves.
This package requires Go 1.27 or newer. Consumers that cannot upgrade their toolchain can remain on v2.
Existing users should read Migrating to v4 for the complete API and ownership changes.
go get github.com/goforj/collection/v4| Group | Functions and methods |
|---|---|
| Aggregation | Avg · CountBy · CountByValue · Max · MaxBy · Median · Min · MinBy · Mode · Reduce · Sum |
| Construction | Clone · New |
| Debugging | Dd · Dump · DumpStr · Slice.Dump |
| Grouping | GroupBy |
| Maps | FromMap · ToMap |
| Ordering | After · Reverse · Shuffle · Sort |
| Querying | All · Any · At · First · FirstWhere · IndexWhere · Last · LastWhere · None |
| Set Operations | Difference · Intersect · SymmetricDifference · Union · Unique · UniqueBy · UniqueComparable |
| Slicing | Chunk · Filter · Partition · Retain · Skip · SkipLast · Take · TakeLast · TakeUntil · Window |
| Transformation | Concat · Each · Map · Multiply · Prepend · Tap · Times · Transform · Zip · ZipWith |
Avg returns the average of the numeric slice values as a float64. If the slice is empty, Avg returns 0.
Example: integers
collection.Dump(collection.Avg([]int{2, 4, 6}))
// 4.000000 #float64Example: float
collection.Dump(collection.Avg([]float64{1.5, 2.5, 3.0}))
// 2.333333 #float64CountBy returns occurrence counts keyed by the extracted value.
numbers := collection.New([]int{1, 2, 3, 5})
counts := numbers.CountBy(func(number int) string {
if number%2 == 0 {
return "even"
}
return "odd"
})
collection.Dump(counts)
// #map[string]int {
// even => 1 #int
// odd => 3 #int
// }CountByValue returns the number of occurrences of each distinct item in c.
T must be comparable.
collection.Dump(collection.CountByValue([]string{"go", "forj", "go"}))
// #map[string]int {
// forj => 1 #int
// go => 2 #int
// }Max returns the largest item in a numeric slice. The second return value is false if the slice is empty.
Example: integers
values := []int{3, 1, 2}
max1, ok1 := collection.Max(values)
collection.Dump(max1, ok1)
// 3 #int
// true #boolExample: floats
values2 := []float64{1.5, 9.2, 4.4}
max2, ok2 := collection.Max(values2)
collection.Dump(max2, ok2)
// 9.200000 #float64
// true #boolExample: empty numeric slice
empty := []int{}
max3, ok3 := collection.Max(empty)
collection.Dump(max3, ok3)
// 0 #int
// false #boolMaxBy returns the item whose extracted key is the largest.
words := collection.New([]string{"pear", "fig", "banana"})
longest, ok := words.MaxBy(func(word string) int {
return len(word)
})
collection.Dump(longest, ok)
// "banana" #string
// true #boolMedian returns the statistical median of a numeric slice as float64. It returns (0, false) if the slice is empty. Median copies the input before sorting, so it allocates O(n) storage and does not mutate the input slice.
- Odd count: middle value.
- Even count: average of the two middle values.
Example: integers - odd number of items
values := []int{3, 1, 2}
median1, ok1 := collection.Median(values)
collection.Dump(median1, ok1)
// 2.000000 #float64
// true #boolExample: integers - even number of items
values2 := []int{10, 2, 4, 6}
median2, ok2 := collection.Median(values2)
collection.Dump(median2, ok2)
// 5.000000 #float64
// true #boolExample: floats
values3 := []float64{1.1, 9.9, 3.3}
median3, ok3 := collection.Median(values3)
collection.Dump(median3, ok3)
// 3.300000 #float64
// true #boolExample: integers - empty numeric slice
empty := []int{}
median4, ok4 := collection.Median(empty)
collection.Dump(median4, ok4)
// 0.000000 #float64
// false #boolMin returns the smallest item in a numeric slice. The second return value is false if the slice is empty.
Example: integers
values := []int{3, 1, 2}
min, ok := collection.Min(values)
collection.Dump(min, ok)
// 1 #int
// true #boolExample: floats
values2 := []float64{2.5, 9.1, 1.2}
min2, ok2 := collection.Min(values2)
collection.Dump(min2, ok2)
// 1.200000 #float64
// true #boolExample: integers - empty collection
empty := []int{}
min3, ok3 := collection.Min(empty)
collection.Dump(min3, ok3)
// 0 #int
// false #boolMinBy returns the item whose extracted key is the smallest.
words := collection.New([]string{"pear", "fig", "banana"})
shortest, ok := words.MinBy(func(word string) int {
return len(word)
})
collection.Dump(shortest, ok)
// "fig" #string
// true #boolMode returns the most frequent numeric value or values in a slice. If multiple values tie for highest frequency, all are returned in first-seen order.
Example: integers - single mode
collection.Dump(collection.Mode([]int{1, 2, 2, 3}))
// #[]int [
// 0 => 2 #int
// ]Example: integers - tie for mode
collection.Dump(collection.Mode([]int{1, 2, 1, 2}))
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// ]Example: floats
collection.Dump(collection.Mode([]float64{1.1, 2.2, 1.1, 3.3}))
// #[]float64 [
// 0 => 1.100000 #float64
// ]Example: integers - empty collection
collection.Dump(collection.Mode([]int{}))
// []int(nil)Reduce collapses the collection into a single accumulated value. The accumulator may have a different type R from the collection's elements.
This is useful for computing sums, concatenations, aggregates, or any fold-style reduction.
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 items in a numeric slice. If the slice is empty, Sum returns the zero value of T.
Example: integers
collection.Dump(collection.Sum([]int{1, 2, 3}))
// 6 #intExample: floats
collection.Dump(collection.Sum([]float64{1.5, 2.5}))
// 4.000000 #float64Example: integers - empty collection
collection.Dump(collection.Sum([]int{}))
// 0 #intClone returns a copy of the collection.
The returned collection has its own backing slice, so element assignments and slice operations on the clone do not affect the original collection. Clone is shallow: pointers, maps, slices, and other references stored in elements remain shared.
Clone is intended to be used when branching a pipeline while preserving the original collection.
Example: basic cloning
c := collection.New([]int{1, 2, 3})
clone := c.Clone()
clone.Transform(func(value int) int { return value * 10 })
collection.Dump(c)
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// ]
collection.Dump(clone)
// #[]int [
// 0 => 10 #int
// 1 => 20 #int
// 2 => 30 #int
// ]Example: branching pipelines
base := collection.New([]int{1, 2, 3, 4, 5})
evens := base.Clone().Retain(func(v int) bool {
return v%2 == 0
})
odds := base.Clone().Retain(func(v int) bool {
return v%2 != 0
})
collection.Dump(base)
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// 3 => 4 #int
// 4 => 5 #int
// ]
collection.Dump(evens)
// #[]int [
// 0 => 2 #int
// 1 => 4 #int
// ]
collection.Dump(odds)
// #[]int [
// 0 => 1 #int
// 1 => 3 #int
// 2 => 5 #int
// ]New creates a Slice from items and borrows their backing array.
values := collection.New([]int{10, 20, 30})
fmt.Println(len(values))
// 3
fmt.Println(values[1])
// 20
total := 0
for _, value := range values {
total += value
}
fmt.Println(total)
// 60Dd prints items then terminates execution. Like Laravel's dd(), this is intended for debugging and should not be used in production control flow.
This method never returns.
collection.New([]string{"a", "b"}).Dd()
// #[]string [
// 0 => "a" #string
// 1 => "b" #string
// ]
// Process finished with the exit code 1Dump is a convenience function that calls godump.Dump.
collection.Dump(collection.New([]int{1, 2, 3}))
// #[]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.
fmt.Println(collection.New([]int{10, 20}).DumpStr())
// #[]int [
// 0 => 10 #int
// 1 => 20 #int
// ]Dump prints items with godump and returns the same collection. This is a no-op on the collection itself.
Example: integers
collection.New([]int{1, 2, 3}).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
// ]GroupBy partitions this Slice into independent built-in slices keyed by the extracted value.
numbers := collection.New([]int{1, 2, 3, 4})
groups := numbers.GroupBy(func(number int) string {
if number%2 == 0 {
return "even"
}
return "odd"
})
collection.Dump(groups["even"], groups["odd"])
// #[]int [
// 0 => 2 #int
// 1 => 4 #int
// ]
// #[]int [
// 0 => 1 #int
// 1 => 3 #int
// ]
fmt.Println(len(groups["even"]))
// 2
fmt.Println(groups["odd"][0])
// 1
collection.Dump(groups["even"][:1])
// #[]int [
// 0 => 2 #int
// ]FromMap materializes a map into a collection of key/value pairs.
The iteration order of the resulting collection is unspecified, matching Go's map iteration semantics.
This function does not mutate the input map.
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.First < b.First
})
collection.Dump(c)
// #[]collection.Pair[string,int] [
// 0 => #collection.Pair[string,int] {
// +First => "a" #string
// +Second => 1 #int
// }
// 1 => #collection.Pair[string,int] {
// +First => "b" #string
// +Second => 2 #int
// }
// 2 => #collection.Pair[string,int] {
// +First => "c" #string
// +Second => 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.Second.Enabled
}).
Sort(func(a, b collection.Pair[string, Config]) bool {
return a.First < b.First
})
collection.Dump(out)
// #[]collection.Pair[string,main.Config·1] [
// 0 => #collection.Pair[string,main.Config·1] {
// +First => "router-1" #string
// +Second => #main.Config {
// +Enabled => true #bool
// +Timeout => 30 #int
// }
// }
// 1 => #collection.Pair[string,main.Config·1] {
// +First => "router-3" #string
// +Second => #main.Config {
// +Enabled => true #bool
// +Timeout => 45 #int
// }
// }
// ]ToMap reduces this collection into a map using the provided key and value functions. If multiple items produce the same key, the value derived from the last item wins.
words := collection.New([]string{"go", "forj"})
lengths := words.ToMap(
func(word string) string { return word },
func(word string) int { return len(word) },
)
collection.Dump(lengths)
// #map[string]int {
// forj => 4 #int
// go => 2 #int
// }After returns all items after the first element for which pred returns true. If no element matches, an empty collection is returned.
NOTE: returns a view (shares backing array). Use Clone() to detach.
collection.New([]int{1, 2, 3, 4, 5}).After(func(v int) bool { return v == 3 }).Dump()
// #[]int [
// 0 => 4 #int
// 1 => 5 #int
// ]Reverse reverses the order of items in the collection in place and returns the same collection for chaining.
This operation performs no allocations.
Example: integers
c := collection.New([]int{1, 2, 3, 4})
c.Reverse()
collection.Dump(c)
// #[]int [
// 0 => 4 #int
// 1 => 3 #int
// 2 => 2 #int
// 3 => 1 #int
// ]Example: strings - chaining
out := collection.New([]string{"a", "b", "c"}).
Reverse().
Concat([]string{"d"})
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)
// #[]main.User [
// 0 => #main.User {
// +ID => 3 #int
// }
// 1 => #main.User {
// +ID => 2 #int
// }
// 2 => #main.User {
// +ID => 1 #int
// }
// ]Shuffle shuffles the collection in place and returns the same collection.
This operation mutates the receiver's backing slice.
Example: integers
c := collection.New([]int{1, 2, 3, 4, 5})
c.Shuffle()
fmt.Println(len(c), collection.Sum(c))
// 5 15Example: strings - chaining
out2 := collection.New([]string{"a", "b", "c"}).
Shuffle().
Concat([]string{"d"})
fmt.Println(len(out2))
// 4Example: structs
type User struct {
ID int
}
users := collection.New([]User{
{ID: 1},
{ID: 2},
{ID: 3},
{ID: 4},
})
users.Shuffle()
fmt.Println(len(users))
// 4Sort sorts the collection in place using the provided comparison function and returns the same collection for chaining.
The comparison function less(a, b) should return true if a should come
before b in the sorted order.
This operation mutates the underlying slice and does not allocate a new element backing slice. The underlying sort implementation may make small internal allocations.
Example: integers
c := collection.New([]int{5, 1, 4, 2})
c.Sort(func(a, b int) bool { return a < b })
collection.Dump(c)
// #[]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)
// #[]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)
// #[]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
collection.Dump(collection.New([]int{2, 4, 6}).All(func(v int) bool { return v%2 == 0 }))
// true #boolExample: integers - not all even
collection.Dump(collection.New([]int{2, 3, 4}).All(func(v int) bool { return v%2 == 0 }))
// false #boolExample: strings - all non-empty
collection.Dump(collection.New([]string{"a", "b", "c"}).All(func(s string) bool { return s != "" }))
// true #boolExample: empty collection (vacuously true)
collection.Dump(collection.New([]int{}).All(func(v int) bool { return v > 0 }))
// true #boolAny returns true if at least one item satisfies fn.
collection.Dump(collection.New([]int{1, 2, 3, 4}).Any(func(v int) bool { return v%2 == 0 }))
// true #boolAt returns the item at the given index and a boolean indicating whether the index was within bounds.
This method is safe and does not panic for out-of-range indices.
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 #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.
This method is equivalent to Laravel's collection->first(fn) and mirrors the behavior found in functional collections in other languages.
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).
This operation performs no allocations and short-circuits on the first match.
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 #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
collection.Dump(collection.New([]int{1, 3, 5}).None(func(v int) bool { return v%2 == 0 }))
// true #boolExample: integers - some even
collection.Dump(collection.New([]int{1, 2, 3}).None(func(v int) bool { return v%2 == 0 }))
// false #boolExample: empty collection
collection.Dump(collection.New([]int{}).None(func(v int) bool { return v > 0 }))
// true #boolDifference 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})
collection.Dump(collection.Difference(a, b))
// #[]int [
// 0 => 1 #int
// 1 => 3 #int
// ]Example: strings
left := collection.New([]string{"apple", "banana", "cherry"})
right := collection.New([]string{"banana"})
collection.Dump(collection.Difference(left, right))
// #[]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"},
})
collection.Dump(collection.Difference(groupA, groupB))
// #[]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.
Order follows the second collection. Duplicates are preserved based on the second collection.
Example: integers
a := collection.New([]int{1, 2, 2, 3, 4})
b := collection.New([]int{2, 4, 4, 5})
collection.Dump(collection.Intersect(a, b))
// #[]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"})
collection.Dump(collection.Intersect(left, right))
// #[]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"},
})
collection.Dump(collection.Intersect(groupA, groupB))
// #[]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})
collection.Dump(collection.SymmetricDifference(a, b))
// #[]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"})
collection.Dump(collection.SymmetricDifference(left, right))
// #[]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"},
})
collection.Dump(collection.SymmetricDifference(groupA, groupB))
// #[]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})
collection.Dump(collection.Union(a, b))
// #[]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"})
collection.Dump(collection.Union(left, right))
// #[]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"},
})
collection.Dump(collection.Union(groupA, groupB))
// #[]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.
The eq function should return true when two values are considered equal.
Example: integers
c1 := collection.New([]int{1, 2, 2, 3, 4, 4, 5})
collection.Dump(c1.Unique(func(a, b int) bool { return a == b }))
// #[]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)
// #[]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)
// #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// +Name => "Alice" #string
// }
// 1 => #main.User {
// +ID => 2 #int
// +Name => "Bob" #string
// }
// ]UniqueBy returns a collection containing the first item for each extracted key.
words := collection.New([]string{"go", "up", "forj", "code"})
unique := words.UniqueBy(func(word string) int {
return len(word)
})
collection.Dump(unique)
// #[]string [
// 0 => "go" #string
// 1 => "forj" #string
// ]UniqueComparable returns a new collection with duplicate comparable items removed. The first occurrence of each value is kept, and order is preserved. It uses a map to track seen values, so it has expected linear time and allocates storage for both the map and the result.
Example: integers
collection.Dump(collection.UniqueComparable([]int{1, 2, 2, 3, 4, 4, 5}))
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// 3 => 4 #int
// 4 => 5 #int
// ]Example: strings
collection.Dump(collection.UniqueComparable([]string{"A", "a", "B", "B"}))
// #[]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.
If size <= 0, nil is returned.
Chunk allocates the outer result slice. Each chunk is a capacity-capped view that shares the backing array with the source collection.
Example: integers
collection.Dump(collection.New([]int{1, 2, 3, 4, 5}).Chunk(2))
// #[][]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)
// #[][]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.
Filter allocates a new Slice and leaves c and its backing storage unchanged.
Example: integers
source := collection.New([]int{1, 2, 3, 4})
filtered := source.Filter(func(v int) bool {
return v%2 == 0
})
collection.Dump(filtered)
// #[]int [
// 0 => 2 #int
// 1 => 4 #int
// ]
fmt.Println(source[0])
// 1Example: strings
c2 := collection.New([]string{"apple", "banana", "cherry", "avocado"})
c2 = c2.Filter(func(v string) bool {
return strings.HasPrefix(v, "a")
})
collection.Dump(c2)
// #[]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 = users.Filter(func(u User) bool {
return strings.HasPrefix(u.Name, "A")
})
collection.Dump(users)
// #[]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 slices based on predicate fn. The first slice 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, odds)
// #[]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, other)
// #[]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, inactive)
// #[]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
// }
// ]Retain keeps items for which fn returns true in c's existing backing array.
Retain returns a capacity-capped, shortened slice header, so callers should retain its result when subsequent operations must observe the new length.
values := collection.New([]int{1, 2, 3, 4})
evens := values.Retain(func(value int) bool { return value%2 == 0 })
collection.Dump(evens)
// #[]int [
// 0 => 2 #int
// 1 => 4 #int
// ]
fmt.Println(values)
// [2 4 0 0]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.
This operation performs no element allocations; it re-slices the underlying slice.
NOTE: returns a view (shares backing array). Use Clone() to detach.
Example: integers
c := collection.New([]int{1, 2, 3, 4, 5})
out := c.Skip(2)
collection.Dump(out)
// #[]int [
// 0 => 3 #int
// 1 => 4 #int
// 2 => 5 #int
// ]Example: skip none
out2 := c.Skip(0)
collection.Dump(out2)
// #[]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)
// #[]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)
// #[]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.
This operation performs no element allocations; it re-slices the underlying slice.
NOTE: returns a view (shares backing array). Use Clone() to detach.
Example: integers
c := collection.New([]int{1, 2, 3, 4, 5})
out := c.SkipLast(2)
collection.Dump(out)
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// ]Example: skip none
out2 := c.SkipLast(0)
collection.Dump(out2)
// #[]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)
// #[]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)
// #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// }
// 1 => #main.User {
// +ID => 2 #int
// }
// ]Take returns a capacity-capped view containing the first n items.
If n exceeds the collection length, the entire collection is returned. If n == 0, an empty collection is returned.
NOTE: returns a view (shares backing array). Use Clone() to detach.
Example: integers - take first 3
c1 := collection.New([]int{0, 1, 2, 3, 4, 5})
out1 := c1.Take(3)
collection.Dump(out1)
// #[]int [
// 0 => 0 #int
// 1 => 1 #int
// 2 => 2 #int
// ]Example: integers - n exceeds length → whole collection
c3 := collection.New([]int{10, 20})
out3 := c3.Take(10)
collection.Dump(out3)
// #[]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)
// #[]int [
// ]TakeLast returns a capacity-capped view 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.
This operation performs no element allocations; it re-slices the underlying slice.
NOTE: returns a view (shares backing array). Use Clone() to detach.
Example: integers
c := collection.New([]int{1, 2, 3, 4, 5})
out := c.TakeLast(2)
collection.Dump(out)
// #[]int [
// 0 => 4 #int
// 1 => 5 #int
// ]Example: take none
out2 := c.TakeLast(0)
collection.Dump(out2)
// #[]int [
// ]Example: take all
out3 := c.TakeLast(10)
collection.Dump(out3)
// #[]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)
// #[]main.User [
// 0 => #main.User {
// +ID => 3 #int
// }
// ]TakeUntil returns items until the predicate function returns true. The matching item is NOT included.
NOTE: returns a view (shares backing array). Use Clone() to detach.
Example: integers - stop when value >= 3
c1 := collection.New([]int{1, 2, 3, 4})
out1 := c1.TakeUntil(func(v int) bool { return v >= 3 })
collection.Dump(out1)
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// ]Example: integers - predicate immediately true → empty result
c2 := collection.New([]int{10, 20, 30})
out2 := c2.TakeUntil(func(v int) bool { return v < 50 })
collection.Dump(out2)
// #[]int [
// ]Example: integers - no match → full list returned
c3 := collection.New([]int{1, 2, 3})
out3 := c3.TakeUntil(func(v int) bool { return v == 99 })
collection.Dump(out3)
// #[]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.
Window allocates the outer result slice. Each window is a capacity-capped view that shares the backing array with the source collection.
Example: integers - step 1
collection.Dump(collection.New([]int{1, 2, 3, 4, 5}).Window(3, 1))
// #[][]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
collection.Dump(collection.New([]string{"a", "b", "c", "d", "e"}).Window(2, 2))
// #[][]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 := points.Window(2, 1)
collection.Dump(win3)
// #[][]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
// }
// ]
// ]Concat returns an independent collection containing c followed by values.
Callers must capture the returned Slice because a value receiver cannot extend c's slice header. The returned collection never shares backing storage with c.
Example: strings
c := collection.New([]string{"John Doe"})
concatenated := c.
Concat([]string{"Jane Doe"}).
Concat([]string{"Johnny Doe"})
collection.Dump(concatenated)
// #[]string [
// 0 => "John Doe" #string
// 1 => "Jane Doe" #string
// 2 => "Johnny Doe" #string
// ]Example: spare capacity
backing := make([]int, 2, 4)
copy(backing, []int{1, 2})
values := collection.New(backing)
values = values.Concat([]int{3, 4})
fmt.Println(values)
// [1 2 3 4]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 maps this Slice to a newly allocated Slice with a potentially different element type.
numbers := collection.New([]int{1, 2, 3, 4})
labels := numbers.Map(func(number int) string {
if number%2 == 0 {
return "even"
}
return "odd"
})
collection.Dump(labels)
// #[]string [
// 0 => "odd" #string
// 1 => "even" #string
// 2 => "odd" #string
// 3 => "even" #string
// ]
fmt.Println(numbers[0])
// 1Multiply creates n copies of all items in the collection
and returns a new collection.
Example: integers
ints := collection.New([]int{1, 2})
collection.Dump(ints.Multiply(3))
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 1 #int
// 3 => 2 #int
// 4 => 1 #int
// 5 => 2 #int
// ]Example: strings
collection.Dump(collection.New([]string{"a", "b"}).Multiply(2))
// #[]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"}})
collection.Dump(users.Multiply(2))
// #[]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
collection.Dump(ints.Multiply(0))
// #[]int [
// ]Prepend returns an independently backed Slice containing values followed by c.
It allocates exactly enough storage for the result and leaves c unchanged.
Example: integers
c := collection.New([]int{3, 4})
result := c.Prepend(1, 2)
collection.Dump(result)
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// 2 => 3 #int
// 3 => 4 #int
// ]Example: strings
letters := collection.New([]string{"c", "d"})
result2 := letters.Prepend("a", "b")
collection.Dump(result2)
// #[]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"},
})
result3 := users.Prepend(User{ID: 1, Name: "Alice"})
collection.Dump(result3)
// #[]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{})
result4 := empty.Prepend(9, 8)
collection.Dump(result4)
// #[]int [
// 0 => 9 #int
// 1 => 8 #int
// ]Example: integers - Prepending no values → no change
c2 := collection.New([]int{1, 2})
result5 := c2.Prepend()
collection.Dump(result5)
// #[]int [
// 0 => 1 #int
// 1 => 2 #int
// ]Tap invokes fn with the Slice value for side effects such as logging, debugging, or inspection, then returns the Slice to allow chaining.
The callback receives a borrowed Slice and may mutate its elements. Use Clone before Tap when the original backing array must remain isolated. The slice header is passed by value, so reslicing, appending, or assigning a shortened Slice inside fn does not change the header returned by Tap.
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.Slice[int]) {
captured1 = append([]int(nil), col...) // 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)
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.Slice[int]) {
collection.Dump(col)
// #[]int [
// 0 => 10 #int
// 1 => 20 #int
// 2 => 30 #int
// ]
}).
Filter(func(v int) bool { return v > 10 })
collection.Dump(c2) // 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.Slice[User]) {
collection.Dump(col)
// #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// +Name => "Alice" #string
// }
// 1 => #main.User {
// +ID => 2 #int
// +Name => "Bob" #string
// }
// ]
})
collection.Dump(users2) // 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 creates a new collection by calling fn(i) for i = 1..count. This mirrors Laravel's Collection::times(), which is 1-indexed.
If count <= 0, an empty collection is returned.
Example: integers - double each index
cTimes1 := collection.Times(5, func(i int) int {
return i * 2
})
collection.Dump(cTimes1)
// #[]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)
// #[]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)
// #[]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 a same-type transformation in place and returns the same collection.
Transform mutates the receiver's backing slice. Use Clone() if you need isolation.
Example: integers
c := collection.New([]int{1, 2, 3})
c.Transform(func(v int) int {
return v * 10
})
collection.Dump(c)
// #[]int [
// 0 => 10 #int
// 1 => 20 #int
// 2 => 30 #int
// ]Example: strings
c2 := collection.New([]string{"apple", "banana", "cherry"})
upper := c2.Transform(func(s string) string {
return strings.ToUpper(s)
})
collection.Dump(upper)
// #[]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.Transform(func(u User) User {
u.Name = strings.ToUpper(u.Name)
return u
})
collection.Dump(updated)
// #[]main.User [
// 0 => #main.User {
// +ID => 1 #int
// +Name => "ALICE" #string
// }
// 1 => #main.User {
// +ID => 2 #int
// +Name => "BOB" #string
// }
// ]Zip combines this collection with values element-wise into pairs. The resulting length is the smaller of the two inputs.
nums := collection.New([]int{1, 2, 3})
words := []string{"one", "two"}
out := nums.Zip(words)
collection.Dump(out)
// #[]collection.Pair[int,string] [
// 0 => #collection.Pair[int,string] {
// +First => 1 #int
// +Second => "one" #string
// }
// 1 => #collection.Pair[int,string] {
// +First => 2 #int
// +Second => "two" #string
// }
// ]ZipWith combines this collection with a slice using fn up to the shorter length.
left := collection.New([]int{1, 2, 3})
right := collection.New([]int{10, 20})
sums := left.ZipWith(right, func(a, b int) int {
return a + b
})
collection.Dump(sums)
// #[]int [
// 0 => 11 #int
// 1 => 22 #int
// ]Use make test for the root module, make vet for static checks, and make generate to refresh the generated README API reference. The docs and examples directories are separate Go modules and can be tested from their own directories when changed.
