Skip to content
Support this libraryStar on GitHub

goforj/collection logo

Fluent collections for Go. Iterate, filter, transform, sort, reduce, group, and debug data with a tiny dependency footprint.

Go ReferenceLicense: MITGo TestGo versionLatest tagTests

Features ​

  • 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 godump for 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, and encoding/json directly
  • Developer-friendly debug helpers (Dump(), Dd(), DumpStr())
  • Works with any Go type, including structs, pointers, and deeply nested composites

Fluent Chaining ​

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

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:

go
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))
// 2

Performance Benchmarks ​

Equivalent 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.

Read-only scalar ops ​

OpSpeed vs loMemoryAllocs
All≈≈≈
Any≈≈≈
None≈≈≈
Firstbelow floor≈≈
Lastbelow floor≈≈
FirstWheresame loop≈≈
IndexWhere≈≈≈
slices.Contains≈≈≈
Reduce (sum)≈≈≈
Sum≈≈≈
Min≈≈≈
Max≈≈≈
Each≈≈≈

Transforming ops ​

OpSpeed vs loMemoryAllocs
Chunkview trade-offownership trade-offownership trade-off
Filterinconclusive≈≈
Mapinconclusive≈≈
Takebelow floor≈≈
Skipview trade-offownership trade-offownership trade-off
SkipLastview trade-offownership trade-offownership trade-off
Zipinconclusive≈≈
ZipWith1.9x faster≈≈
UniqueComparable≈≈≈
UniqueBy≈≈≈
Unioninconclusive≈≈
Intersect1.6x faster-33.8KB+10
Differencedifferent workAPI trade-offAPI trade-off
GroupBy≈≈≈
CountBy≈≈≈
CountByValue≈≈≈
ToMap≈≈≈

Pipelines ​

OpSpeed vs loMemoryAllocs
Pipeline F→M→T→R1.5x slower≈≈

Mutating ops ​

OpSpeed vs loMemoryAllocs
Retain≈≈≈
Reverse≈≈≈
Shuffle3.7x faster≈≈
Transform≈≈≈

How to read the benchmarks ​

  • 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

Allocation and mutation are explicit ​

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, and Prepend return independent results
  • Retain and Transform mutate in place; the shortened result from Retain must be captured
  • Sort, Reverse, and Shuffle mutate elements in place
  • View-producing slicing operations return capacity-capped views
  • Clone creates 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.

Explicit branching with Clone {#explicit-branching-with-clone} ​

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.

go
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.

Design Principles ​

  • 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

What this library is not ​

  • Not a lazy or streaming library
  • Not concurrency-aware
  • Not immutable-by-default
  • Not a replacement for idiomatic loops in simple cases
  • Not designed to hide allocation, mutation, or ordering semantics

Working with maps ​

Maps are unordered in Go. This library does not pretend otherwise.

Instead, map interaction is explicit and intentional:

  • FromMap materializes key/value pairs into a collection; its initial order is unspecified, so sort when a particular order matters
  • ToMap reduces collections back into maps explicitly

This makes map materialization visible and leaves deterministic ordering to an explicit Sort.

Behavior semantics ​

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.

Native slice interoperability ​

Slice[T] is a named slice, so ordinary Go operations work directly:

go
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
// 30

Use slices.Values(values) or slices.All(values) when an iterator is useful; no collection-specific lazy wrapper is required.

Runnable examples ​

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.

Installation ​

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.

bash
go get github.com/goforj/collection/v4

API Index ​

GroupFunctions and methods
AggregationAvg · CountBy · CountByValue · Max · MaxBy · Median · Min · MinBy · Mode · Reduce · Sum
ConstructionClone · New
DebuggingDd · Dump · DumpStr · Slice.Dump
GroupingGroupBy
MapsFromMap · ToMap
OrderingAfter · Reverse · Shuffle · Sort
QueryingAll · Any · At · First · FirstWhere · IndexWhere · Last · LastWhere · None
Set OperationsDifference · Intersect · SymmetricDifference · Union · Unique · UniqueBy · UniqueComparable
SlicingChunk · Filter · Partition · Retain · Skip · SkipLast · Take · TakeLast · TakeUntil · Window
TransformationConcat · Each · Map · Multiply · Prepend · Tap · Times · Transform · Zip · ZipWith

Aggregation ​

Avg - readonly - terminal ​

Avg returns the average of the numeric slice values as a float64. If the slice is empty, Avg returns 0.

Example: integers

go
collection.Dump(collection.Avg([]int{2, 4, 6}))
// 4.000000 #float64

Example: float

go
collection.Dump(collection.Avg([]float64{1.5, 2.5, 3.0}))
// 2.333333 #float64

CountBy - readonly - terminal ​

CountBy returns occurrence counts keyed by the extracted value.

go
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 - readonly - terminal ​

CountByValue returns the number of occurrences of each distinct item in c.

T must be comparable.

go
collection.Dump(collection.CountByValue([]string{"go", "forj", "go"}))
// #map[string]int {
//   forj => 1 #int
//   go => 2 #int
// }

Max - readonly - terminal ​

Max returns the largest item in a numeric slice. The second return value is false if the slice is empty.

Example: integers

go
values := []int{3, 1, 2}

max1, ok1 := collection.Max(values)
collection.Dump(max1, ok1)
// 3 #int
// true #bool

Example: floats

go
values2 := []float64{1.5, 9.2, 4.4}

max2, ok2 := collection.Max(values2)
collection.Dump(max2, ok2)
// 9.200000 #float64
// true #bool

Example: empty numeric slice

go
empty := []int{}

max3, ok3 := collection.Max(empty)
collection.Dump(max3, ok3)
// 0 #int
// false #bool

MaxBy - readonly - terminal ​

MaxBy returns the item whose extracted key is the largest.

go
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 #bool

Median - readonly - terminal ​

Median 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

go
values := []int{3, 1, 2}

median1, ok1 := collection.Median(values)
collection.Dump(median1, ok1)
// 2.000000 #float64
// true #bool

Example: integers - even number of items

go
values2 := []int{10, 2, 4, 6}

median2, ok2 := collection.Median(values2)
collection.Dump(median2, ok2)
// 5.000000 #float64
// true #bool

Example: floats

go
values3 := []float64{1.1, 9.9, 3.3}

median3, ok3 := collection.Median(values3)
collection.Dump(median3, ok3)
// 3.300000 #float64
// true #bool

Example: integers - empty numeric slice

go
empty := []int{}

median4, ok4 := collection.Median(empty)
collection.Dump(median4, ok4)
// 0.000000 #float64
// false #bool

Min - readonly - terminal ​

Min returns the smallest item in a numeric slice. The second return value is false if the slice is empty.

Example: integers

go
values := []int{3, 1, 2}
min, ok := collection.Min(values)
collection.Dump(min, ok)
// 1 #int
// true #bool

Example: floats

go
values2 := []float64{2.5, 9.1, 1.2}
min2, ok2 := collection.Min(values2)
collection.Dump(min2, ok2)
// 1.200000 #float64
// true #bool

Example: integers - empty collection

go
empty := []int{}
min3, ok3 := collection.Min(empty)
collection.Dump(min3, ok3)
// 0 #int
// false #bool

MinBy - readonly - terminal ​

MinBy returns the item whose extracted key is the smallest.

go
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 #bool

Mode - readonly - terminal ​

Mode 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

go
collection.Dump(collection.Mode([]int{1, 2, 2, 3}))
// #[]int [
//   0 => 2 #int
// ]

Example: integers - tie for mode

go
collection.Dump(collection.Mode([]int{1, 2, 1, 2}))
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
// ]

Example: floats

go
collection.Dump(collection.Mode([]float64{1.1, 2.2, 1.1, 3.3}))
// #[]float64 [
//   0 => 1.100000 #float64
// ]

Example: integers - empty collection

go
collection.Dump(collection.Mode([]int{}))
// []int(nil)

Reduce - readonly - terminal ​

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

go
sum := collection.New([]int{1, 2, 3}).Reduce(0, func(acc, n int) int {
	return acc + n
})
collection.Dump(sum)
// 6 #int

Example: strings

go
joined := collection.New([]string{"a", "b", "c"}).Reduce("", func(acc, s string) string {
	return acc + s
})
collection.Dump(joined)
// "abc" #string

Example: structs

go
type Stats struct {
	Count int
	Sum   int
}

stats := collection.New([]Stats{
	{Count: 1, Sum: 10},
	{Count: 1, Sum: 20},
	{Count: 1, Sum: 30},
})

total := stats.Reduce(Stats{}, func(acc, s Stats) Stats {
	acc.Count += s.Count
	acc.Sum += s.Sum
	return acc
})

collection.Dump(total)
// #main.Stats {
//   +Count => 3 #int
//   +Sum   => 60 #int
// }

Sum - readonly - terminal ​

Sum returns the sum of all items in a numeric slice. If the slice is empty, Sum returns the zero value of T.

Example: integers

go
collection.Dump(collection.Sum([]int{1, 2, 3}))
// 6 #int

Example: floats

go
collection.Dump(collection.Sum([]float64{1.5, 2.5}))
// 4.000000 #float64

Example: integers - empty collection

go
collection.Dump(collection.Sum([]int{}))
// 0 #int

Construction ​

Clone - immutable - chainable ​

Clone 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

go
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

go
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 - immutable - chainable ​

New creates a Slice from items and borrows their backing array.

go
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)
// 60

Debugging ​

Dd - readonly - terminal ​

Dd prints items then terminates execution. Like Laravel's dd(), this is intended for debugging and should not be used in production control flow.

This method never returns.

go
collection.New([]string{"a", "b"}).Dd()
// #[]string [
//   0 => "a" #string
//   1 => "b" #string
// ]
// Process finished with the exit code 1

Dump - readonly - terminal ​

Dump is a convenience function that calls godump.Dump.

go
collection.Dump(collection.New([]int{1, 2, 3}))
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
//   2 => 3 #int
// ]

DumpStr - readonly - terminal ​

DumpStr returns the pretty-printed dump of the items as a string, without printing or exiting. Useful for logging, snapshot testing, and non-interactive debugging.

go
fmt.Println(collection.New([]int{10, 20}).DumpStr())
// #[]int [
//   0 => 10 #int
//   1 => 20 #int
// ]

Slice.Dump - readonly - chainable ​

Dump prints items with godump and returns the same collection. This is a no-op on the collection itself.

Example: integers

go
collection.New([]int{1, 2, 3}).Dump()
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
//   2 => 3 #int
// ]

Example: integers - chaining

go
collection.New([]int{1, 2, 3}).
	Filter(func(v int) bool { return v > 1 }).
	Dump()
// #[]int [
//   0 => 2 #int
//   1 => 3 #int
// ]

Grouping ​

GroupBy - readonly - terminal ​

GroupBy partitions this Slice into independent built-in slices keyed by the extracted value.

go
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
// ]

Maps ​

FromMap - immutable - chainable ​

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

go
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

go
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 - readonly - terminal ​

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.

go
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
// }

Ordering ​

After - immutable - chainable ​

After returns all items after the first element for which pred returns true. If no element matches, an empty collection is returned.

NOTE: returns a view (shares backing array). Use Clone() to detach.

go
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 - mutable - chainable ​

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

go
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

go
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

go
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 - mutable - chainable ​

Shuffle shuffles the collection in place and returns the same collection.

This operation mutates the receiver's backing slice.

Example: integers

go
c := collection.New([]int{1, 2, 3, 4, 5})
c.Shuffle()
fmt.Println(len(c), collection.Sum(c))
// 5 15

Example: strings - chaining

go
out2 := collection.New([]string{"a", "b", "c"}).
	Shuffle().
	Concat([]string{"d"})

fmt.Println(len(out2))
// 4

Example: structs

go
type User struct {
	ID int
}

users := collection.New([]User{
	{ID: 1},
	{ID: 2},
	{ID: 3},
	{ID: 4},
})

users.Shuffle()
fmt.Println(len(users))
// 4

Sort - mutable - chainable ​

Sort 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

go
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)

go
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

go
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
//   }
// ]

Querying ​

All - readonly - terminal ​

All returns true if fn returns true for every item in the collection. If the collection is empty, All returns true (vacuously true).

Example: integers - all even

go
collection.Dump(collection.New([]int{2, 4, 6}).All(func(v int) bool { return v%2 == 0 }))
// true #bool

Example: integers - not all even

go
collection.Dump(collection.New([]int{2, 3, 4}).All(func(v int) bool { return v%2 == 0 }))
// false #bool

Example: strings - all non-empty

go
collection.Dump(collection.New([]string{"a", "b", "c"}).All(func(s string) bool { return s != "" }))
// true #bool

Example: empty collection (vacuously true)

go
collection.Dump(collection.New([]int{}).All(func(v int) bool { return v > 0 }))
// true #bool

Any - readonly - terminal ​

Any returns true if at least one item satisfies fn.

go
collection.Dump(collection.New([]int{1, 2, 3, 4}).Any(func(v int) bool { return v%2 == 0 }))
// true #bool

At - readonly - terminal ​

At 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

go
c := collection.New([]int{10, 20, 30})
v, ok := c.At(1)
collection.Dump(v, ok)
// 20 #int
// true #bool

Example: out of bounds

go
v2, ok2 := c.At(10)
collection.Dump(v2, ok2)
// 0 #int
// false #bool

Example: structs

go
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 #bool

First - readonly - terminal ​

First returns the first element in the collection. If the collection is empty, ok will be false.

Example: integers

go
c := collection.New([]int{10, 20, 30})

v, ok := c.First()
collection.Dump(v, ok)
// 10 #int
// true #bool

Example: strings

go
c2 := collection.New([]string{"alpha", "beta", "gamma"})

v2, ok2 := c2.First()
collection.Dump(v2, ok2)
// "alpha" #string
// true #bool

Example: structs

go
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 #bool

Example: integers - empty collection

go
c3 := collection.New([]int{})
v3, ok4 := c3.First()
collection.Dump(v3, ok4)
// 0 #int
// false #bool

FirstWhere - readonly - terminal ​

FirstWhere returns the first item in the collection for which the provided predicate function returns true. If no items match, ok=false is returned along with the zero value of T.

This method is equivalent to Laravel's collection->first(fn) and mirrors the behavior found in functional collections in other languages.

go
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 #bool

IndexWhere - readonly - terminal ​

IndexWhere returns the index of the first item in the collection for which the provided predicate function returns true. If no item matches, it returns (0, false).

This operation performs no allocations and short-circuits on the first match.

Example: integers

go
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 #bool

Example: not found

go
idx2, ok2 := c.IndexWhere(func(v int) bool { return v == 99 })
collection.Dump(idx2, ok2)
// 0 #int
// false #bool

Example: structs

go
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 #bool

Last - readonly - terminal ​

Last returns the last element in the collection. If the collection is empty, ok will be false.

Example: integers

go
c := collection.New([]int{10, 20, 30})

v, ok := c.Last()
collection.Dump(v, ok)
// 30 #int
// true #bool

Example: strings

go
c2 := collection.New([]string{"alpha", "beta", "gamma"})

v2, ok2 := c2.Last()
collection.Dump(v2, ok2)
// "gamma" #string
// true #bool

Example: structs

go
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 #bool

Example: empty collection

go
c3 := collection.New([]int{})

v3, ok4 := c3.Last()
collection.Dump(v3, ok4)
// 0 #int
// false #bool

LastWhere - readonly - terminal ​

LastWhere returns the last element in the collection that satisfies the predicate fn. If fn is nil, LastWhere returns the final element in the underlying slice. If the collection is empty or no element matches, ok will be false.

Example: integers

go
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 #bool

Example: integers without predicate (equivalent to Last())

go
c2 := collection.New([]int{10, 20, 30, 40})

v2, ok2 := c2.LastWhere(nil)
collection.Dump(v2, ok2)
// 40 #int
// true #bool

Example: strings

go
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 #bool

Example: structs

go
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 #bool

Example: no matching element

go
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 #bool

Example: empty collection

go
c5 := collection.New([]int{})

v5, ok6 := c5.LastWhere(nil)
collection.Dump(v5, ok6)
// 0 #int
// false #bool

None - readonly - terminal ​

None returns true if fn returns false for every item in the collection. If the collection is empty, None returns true.

Example: integers - none even

go
collection.Dump(collection.New([]int{1, 3, 5}).None(func(v int) bool { return v%2 == 0 }))
// true #bool

Example: integers - some even

go
collection.Dump(collection.New([]int{1, 2, 3}).None(func(v int) bool { return v%2 == 0 }))
// false #bool

Example: empty collection

go
collection.Dump(collection.New([]int{}).None(func(v int) bool { return v > 0 }))
// true #bool

Set Operations ​

Difference - immutable - chainable ​

Difference returns a new collection containing elements from the first collection that are not present in the second. Order follows the first collection, and duplicates are removed.

Example: integers

go
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

go
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

go
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 - immutable - chainable ​

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

go
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

go
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

go
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 - immutable - chainable ​

SymmetricDifference returns a new collection containing elements that appear in exactly one of the two collections. Order follows the first collection for its unique items, then the second for its unique items. Duplicates are removed.

Example: integers

go
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

go
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

go
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 - immutable - chainable ​

Union returns a new collection containing the unique elements from both collections. Items from the first collection are kept in order, followed by items from the second that were not already present.

Example: integers

go
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

go
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

go
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 - immutable - chainable ​

Unique returns a new collection with duplicate items removed, based on the equality function eq. The first occurrence of each unique value is kept, and order is preserved.

The eq function should return true when two values are considered equal.

Example: integers

go
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)

go
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)

go
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 - immutable - chainable ​

UniqueBy returns a collection containing the first item for each extracted key.

go
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 - immutable - chainable ​

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

go
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

go
collection.Dump(collection.UniqueComparable([]string{"A", "a", "B", "B"}))
// #[]string [
//   0 => "A" #string
//   1 => "a" #string
//   2 => "B" #string
// ]

Slicing ​

Chunk - readonly - terminal ​

Chunk splits the collection into chunks of the given size. The final chunk may be smaller if len(items) is not divisible by size.

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

go
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

go
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 - immutable - chainable ​

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

go
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])
// 1

Example: strings

go
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

go
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 - immutable - terminal ​

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

go
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

go
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

go
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 - mutable - chainable ​

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.

go
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 - immutable - chainable ​

Skip returns a new collection with the first n items skipped. If n is less than or equal to zero, Skip returns the full collection. If n is greater than or equal to the collection length, Skip returns an empty collection.

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

go
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

go
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

go
out3 := c.Skip(10)
collection.Dump(out3)
// #[]int [
// ]

Example: structs

go
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 - immutable - chainable ​

SkipLast returns a new collection with the last n items skipped. If n is less than or equal to zero, SkipLast returns the full collection. If n is greater than or equal to the collection length, SkipLast returns an empty collection.

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

go
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

go
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

go
out3 := c.SkipLast(10)
collection.Dump(out3)
// #[]int [
// ]

Example: structs

go
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 - immutable - chainable ​

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

go
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

go
c3 := collection.New([]int{10, 20})
out3 := c3.Take(10)
collection.Dump(out3)
// #[]int [
//	0 => 10 #int
//	1 => 20 #int
// ]

Example: integers - zero → empty

go
c4 := collection.New([]int{1, 2, 3})
out4 := c4.Take(0)
collection.Dump(out4)
// #[]int [
// ]

TakeLast - immutable - chainable ​

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

go
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

go
out2 := c.TakeLast(0)
collection.Dump(out2)
// #[]int [
// ]

Example: take all

go
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

go
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 - immutable - chainable ​

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

go
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

go
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

go
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 - readonly - terminal ​

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

go
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

go
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

go
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
//     }
//   ]
// ]

Transformation ​

Concat - immutable - chainable ​

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

go
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

go
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 - readonly - chainable ​

Each runs fn for every item in the collection and returns the same collection, so it can be used in chains for side effects (logging, debugging, etc.).

Example: integers

go
c := collection.New([]int{1, 2, 3})

sum := 0
c.Each(func(v int) {
	sum += v
})

collection.Dump(sum)
// 6 #int

Example: strings

go
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

go
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 - immutable - chainable ​

Map maps this Slice to a newly allocated Slice with a potentially different element type.

go
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])
// 1

Multiply - immutable - chainable ​

Multiply creates n copies of all items in the collection and returns a new collection.

Example: integers

go
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

go
collection.Dump(collection.New([]string{"a", "b"}).Multiply(2))
// #[]string [
//   0 => "a" #string
//   1 => "b" #string
//   2 => "a" #string
//   3 => "b" #string
// ]

Example: structs

go
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

go
collection.Dump(ints.Multiply(0))
// #[]int [
// ]

Prepend - immutable - chainable ​

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

go
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

go
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

go
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

go
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

go
c2 := collection.New([]int{1, 2})
result5 := c2.Prepend()
collection.Dump(result5)
// #[]int [
//   0 => 1 #int
//   1 => 2 #int
// ]

Tap - mutable - chainable ​

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

go
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

go
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

go
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 - immutable - chainable ​

Times creates a new collection by calling fn(i) for i = 1..count. This mirrors Laravel's Collection::times(), which is 1-indexed.

If count <= 0, an empty collection is returned.

Example: integers - double each index

go
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

go
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

go
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 - mutable - chainable ​

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

go
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

go
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

go
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 - immutable - terminal ​

Zip combines this collection with values element-wise into pairs. The resulting length is the smaller of the two inputs.

go
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 - immutable - chainable ​

ZipWith combines this collection with a slice using fn up to the shorter length.

go
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
// ]

Development ​

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.