go get github.com/CreateLab/glinqpackage main
import (
"fmt"
"github.com/CreateLab/glinq/pkg/glinq"
)
func main() {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
result := glinq.From(numbers).
Where(func(x int) bool { return x > 5 }).
Select(func(x int) int { return x * 2 }).
ToSlice()
fmt.Println(result) // [12, 14, 16, 18, 20]
}- Lazy Evaluation: Operations don't execute until a terminal operation is called
- Method Chaining: Most operations return
Stream[T]for fluent chaining - Type Safety: Full generics support ensures compile-time type checking
glinq separates interfaces into three levels:
The minimal interface for iterable collections. Any type can implement it for integration with glinq:
type Enumerable[T any] interface {
Next() (T, bool)
}Optional interface that extends Enumerable with size information for performance optimization:
type Sizable[T any] interface {
Enumerable[T]
Size() (int, bool) // Returns size and true if known, or 0 and false if unknown
}Note: This is an optional interface - not all Enumerables need to implement it. Size information is used internally for optimizations (preallocation, O(1) Count, etc.).
Extends both Enumerable and Sizable, and adds all operators (Where, Select, OrderBy, etc.):
type Stream[T any] interface {
Enumerable[T]
Sizable[T] // Provides Size() method
Where(predicate func(T) bool) Stream[T]
Select(mapper func(T) T) Stream[T]
// ... more methods
}Enumerable: Minimal interface for iteration - universal and works with any iteratorSizable: Optional size hint for performance optimization - allows O(1) operations when size is knownStream: Full-featured interface with all operators and convenient chaining syntax- Custom Sources: You can implement
Enumerable(orSizable) for your own data sources
type MyIterator struct {
data []int
pos int
}
func (m *MyIterator) Next() (int, bool) {
if m.pos < len(m.data) {
val := m.data[m.pos]
m.pos++
return val, true
}
return 0, false
}
// Now you can use it with glinq functions
iter := &MyIterator{data: []int{1, 2, 3}}
result := glinq.Select(iter, func(x int) int { return x * 2 }).ToSlice()Functions that create a new Stream[T]:
Creates a Stream from a slice:
stream := glinq.From([]int{1, 2, 3})Creates an empty Stream:
empty := glinq.Empty[int]()Creates a Stream of integers:
numbers := glinq.Range(1, 10) // [1, 2, 3, ..., 10]Creates a Stream from a map (returns KeyValue pairs):
m := map[string]int{"a": 1, "b": 2}
stream := glinq.FromMap(m)Converts any Enumerable to Stream:
stream := glinq.FromEnumerable(myEnumerable)Methods that transform the Stream and return a new Stream[T]:
Filters elements by predicate:
evens := glinq.From([]int{1, 2, 3, 4, 5}).
Where(func(x int) bool { return x%2 == 0 }).
ToSlice()
// [2, 4]Transforms elements to the same type:
squared := glinq.From([]int{1, 2, 3}).
Select(func(x int) int { return x * x }).
ToSlice()
// [1, 4, 9]Transforms elements with index:
result := glinq.From([]int{1, 2, 3}).
SelectWithIndex(func(x int, idx int) int { return x * idx }).
ToSlice()
// [0, 2, 6]Takes first n elements:
first3 := glinq.From([]int{1, 2, 3, 4, 5}).Take(3).ToSlice()
// [1, 2, 3]Skips first n elements:
rest := glinq.From([]int{1, 2, 3, 4, 5}).Skip(2).ToSlice()
// [3, 4, 5]Sorts elements using comparator:
sorted := glinq.From([]int{5, 2, 8, 1, 9}).
OrderBy(func(a, b int) int { return a - b }).
ToSlice()
// [1, 2, 5, 8, 9]Note: OrderBy materializes the entire stream for sorting (partially lazy).
Sorts elements in reverse order:
sorted := glinq.From([]int{5, 2, 8, 1, 9}).
OrderByDescending(func(a, b int) int { return a - b }).
ToSlice()
// [9, 8, 5, 2, 1]Removes duplicates by key selector:
type Person struct { ID int; Name string }
people := []Person{{1, "Alice"}, {1, "Alice2"}, {2, "Bob"}}
unique := glinq.From(people).
DistinctBy(func(p Person) any { return p.ID }).
ToSlice()
// [{1, "Alice"}, {2, "Bob"}]Concatenates two streams (preserves duplicates):
result := glinq.From([]int{1, 2}).
Concat(glinq.From([]int{2, 3})).
ToSlice()
// [1, 2, 2, 3]Methods that materialize the Stream:
Converts Stream to slice:
result := glinq.From([]int{1, 2, 3}).ToSlice()Gets first element:
first, ok := glinq.From([]int{1, 2, 3}).First()
// first = 1, ok = trueGets last element:
last, ok := glinq.From([]int{1, 2, 3}).Last()
// last = 3, ok = trueCounts number of elements. Optimized: Returns O(1) if size is known, otherwise O(n):
count := glinq.From([]int{1, 2, 3, 4, 5}).
Where(func(x int) bool { return x > 2 }).
Count()
// 3
// With known size - O(1) operation
count := glinq.From([]int{1, 2, 3, 4, 5}).Count()
// 5 (instant, no iteration needed)Checks if there is at least one element in the Stream. Optimized: Returns O(1) if size is known, otherwise iterates until first element:
hasElements := glinq.From([]int{1, 2, 3}).Any()
// true (O(1) - checks size)
isEmpty := glinq.Empty[int]().Any()
// false (O(1) - checks size)
// With unknown size - iterates until first element
hasAny := glinq.From([]int{1, 2, 3}).
Where(func(x int) bool { return x > 0 }).
Any()
// true (iterates until first match)Checks if any element satisfies predicate:
hasEven := glinq.From([]int{1, 2, 3}).AnyMatch(func(x int) bool { return x%2 == 0 })
// trueReturns the known size of the collection and true, or 0 and false if size is unknown. Used internally for optimizations:
size, known := glinq.From([]int{1, 2, 3, 4, 5}).Size()
// size = 5, known = true
// After filtering, size becomes unknown
size, known := glinq.From([]int{1, 2, 3}).
Where(func(x int) bool { return x > 1 }).
Size()
// size = 0, known = falseChecks if all elements satisfy predicate:
allPositive := glinq.From([]int{1, 2, 3}).All(func(x int) bool { return x > 0 })
// trueExecutes action for each element:
glinq.From([]int{1, 2, 3}).ForEach(func(x int) {
fmt.Println(x)
})Find minimum/maximum using comparator:
type Person struct { Age int; Name string }
people := []Person{{30, "Alice"}, {25, "Bob"}}
youngest, _ := glinq.From(people).Min(func(a, b Person) int {
return a.Age - b.Age
})Applies accumulator function:
sum := glinq.From([]int{1, 2, 3, 4, 5}).
Aggregate(0, func(acc, x int) int { return acc + x })
// 15Splits Stream into chunks:
chunks := glinq.From([]int{1, 2, 3, 4, 5, 6, 7}).Chunk(3)
// [][]int{{1, 2, 3}, {4, 5, 6}, {7}}Standalone functions that transform Enumerable to different types:
Transforms to different type:
strings := glinq.Select(
glinq.From([]int{1, 2, 3}),
func(x int) string { return fmt.Sprintf("num_%d", x) },
).ToSlice()
// []string{"num_1", "num_2", "num_3"}Transforms to different type with index:
strings := glinq.SelectWithIndex(
glinq.From([]int{1, 2, 3}),
func(x int, idx int) string { return fmt.Sprintf("num_%d_at_%d", x, idx) },
).ToSlice()Removes duplicates (requires comparable T):
unique := glinq.Distinct(glinq.From([]int{1, 2, 2, 3, 3, 4})).ToSlice()
// [1, 2, 3, 4]Combines two enumerables and removes duplicates:
set1 := glinq.From([]int{1, 2, 3})
set2 := glinq.From([]int{3, 4, 5})
union := glinq.Union(set1, set2).ToSlice()
// [1, 2, 3, 4, 5]Returns intersection of two enumerables:
intersect := glinq.Intersect(
glinq.From([]int{1, 2, 3}),
glinq.From([]int{2, 3, 4}),
).ToSlice()
// [2, 3]Returns difference of enumerables:
except := glinq.Except(
glinq.From([]int{1, 2, 3}),
glinq.From([]int{2, 3}),
).ToSlice()
// [1]Takes n smallest elements using comparator:
type Person struct { Age int; Name string }
people := []Person{{30, "Alice"}, {25, "Bob"}, {35, "Charlie"}}
youngest := glinq.TakeOrderedBy(
glinq.From(people),
2,
func(a, b Person) bool { return a.Age < b.Age },
).ToSlice()Takes n largest elements using comparator:
oldest := glinq.TakeOrderedDescendingBy(
glinq.From(people),
2,
func(a, b Person) bool { return a.Age < b.Age },
).ToSlice()Functions that work with Enumerable[KeyValue[K, V]]:
Extracts keys from KeyValue pairs:
m := map[string]int{"a": 1, "b": 2}
keys := glinq.Keys(glinq.FromMap(m)).ToSlice()
// []string{"a", "b"}Extracts values from KeyValue pairs:
values := glinq.Values(glinq.FromMap(m)).ToSlice()
// []int{1, 2}Converts Enumerable[KeyValue] to map:
m := map[string]int{"a": 1, "b": 2}
stream := glinq.FromMap(m)
result := glinq.ToMap(stream)Converts Enumerable[T] to map using selectors:
type User struct { ID int; Name string }
users := []User{{1, "Alice"}, {2, "Bob"}}
userMap := glinq.ToMapBy(
glinq.From(users),
func(u User) int { return u.ID },
func(u User) string { return u.Name },
)
// map[int]string{1: "Alice", 2: "Bob"}Functions that work with numeric and ordered types:
Calculates sum of all elements:
sum := glinq.Sum(glinq.From([]int{1, 2, 3, 4, 5}))
// 15Finds minimum element (for ordered types):
min, ok := glinq.Min(glinq.From([]int{5, 2, 8, 1, 9}))
// min = 1, ok = trueFinds maximum element (for ordered types):
max, ok := glinq.Max(glinq.From([]int{5, 2, 8, 1, 9}))
// max = 9, ok = trueNote: For custom types, use Min and Max methods with comparator functions.
result := glinq.From([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}).
Skip(2).
Where(func(x int) bool { return x%2 == 0 }).
Select(func(x int) int { return x * x }).
Take(3).
ToSlice()
// [16, 36, 64]type Person struct {
Name string
Age int
}
people := []Person{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
}
// Sort by age
sorted := glinq.From(people).
OrderBy(func(a, b Person) int { return a.Age - b.Age }).
ToSlice()
// Get youngest
youngest, _ := glinq.From(people).Min(func(a, b Person) int {
return a.Age - b.Age
})// Only processes ~6 elements, not a million!
result := glinq.Range(1, 1000000).
Where(func(x int) bool { return x%2 == 0 }).
Take(3).
ToSlice()
// [2, 4, 6]type Fibonacci struct {
a, b int
}
func (f *Fibonacci) Next() (int, bool) {
next := f.a
f.a, f.b = f.b, f.a+f.b
return next, true
}
fib := &Fibonacci{a: 0, b: 1}
first10 := glinq.FromEnumerable(fib).Take(10).ToSlice()
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]// Good
result := glinq.From(data).
Where(predicate).
Select(mapper).
ToSlice()
// Less ideal (but works)
s1 := glinq.From(data)
s2 := s1.Where(predicate)
s3 := s2.Select(mapper)
result := s3.ToSlice()// Good: Only processes necessary elements
result := glinq.Range(1, 1000000).
Where(expensivePredicate).
Take(10).
ToSlice()
// Less efficient: Processes all elements
all := glinq.Range(1, 1000000).
Where(expensivePredicate).
ToSlice()
result := all[:10]// Use Select function for different types
strings := glinq.Select(
glinq.From([]int{1, 2, 3}),
func(x int) string { return fmt.Sprintf("%d", x) },
).ToSlice()
// Use Select method for same type
doubled := glinq.From([]int{1, 2, 3}).
Select(func(x int) int { return x * 2 }).
ToSlice()// Good: int is comparable
unique := glinq.Distinct(glinq.From([]int{1, 2, 2, 3})).ToSlice()
// For non-comparable types, use DistinctBy
type Person struct { ID int; Name string }
unique := glinq.From(people).
DistinctBy(func(p Person) any { return p.ID }).
ToSlice()glinq automatically tracks size information when possible and uses it for performance optimizations:
These operations maintain size information (1-to-1 transformations):
Select/SelectWithIndex- transforms each elementOrderBy/OrderByDescending- sorts (materializes, but preserves count)Reverse- reverses order (materializes, but preserves count)Take- calculates new size asmin(sourceSize, n)Skip- calculates new size asmax(0, sourceSize - n)Concat- adds sizes if both sources have known size
These operations lose size information (unknown result count):
Where- depends on how many elements pass the filterDistinctBy/Distinct- depends on how many duplicates existSelectMany- 1-to-many transformationUnion/Intersect/Except- depends on overlap
When size is known, these operations are optimized:
-
Count()- O(1) instead of O(n)// O(1) - no iteration needed count := glinq.From([]int{1, 2, 3, 4, 5}).Count() // O(n) - must iterate count := glinq.From([]int{1, 2, 3}). Where(func(x int) bool { return x > 1 }). Count()
-
Any()- O(1) instead of iterating until first element// O(1) - checks size hasElements := glinq.From([]int{1, 2, 3}).Any() // Iterates until first element hasAny := glinq.From([]int{1, 2, 3}). Where(func(x int) bool { return x > 0 }). Any()
-
ToSlice()- Preallocates capacity to avoid reallocations// Preallocates capacity = 5, no reallocations result := glinq.From([]int{1, 2, 3, 4, 5}). Select(func(x int) int { return x * 2 }). ToSlice() // No preallocation, may cause reallocations result := glinq.From([]int{1, 2, 3}). Where(func(x int) bool { return x > 1 }). ToSlice()
-
Chunk()- Preallocates result slice capacity// Preallocates capacity for chunks chunks := glinq.From([]int{1, 2, 3, 4, 5, 6, 7}).Chunk(3)
Note: Size information is tracked automatically - you don't need to do anything special. Operations that preserve size will maintain it, operations that lose size will mark it as unknown (-1).
- Early Termination: Operations stop when enough elements are collected
- Memory Efficient: Only processes what's needed
- Composable: Can chain many operations without materializing intermediate results
Some operations materialize the entire stream:
OrderBy/OrderByDescending- sorts entire collectionIntersect/Except- materializes second enumerable into a setChunk- needs to see all elements to create chunks
- Stream operations: O(1) memory (lazy)
- Set operations (Union, Intersect, Except): O(n) memory for seen elements
- OrderBy: O(n) memory (materializes entire stream)
- Use
Takeearly in chains to limit processing - Avoid
OrderByon very large streams if possible - Use
DistinctByinstead ofDistinctfor non-comparable types - Consider materializing once if you need to iterate multiple times
Required for: Distinct, Union, Intersect, Except
// Works: int is comparable
glinq.Distinct(glinq.From([]int{1, 2, 2}))
// Doesn't compile: slice is not comparable
glinq.Distinct(glinq.From([][]int{{1}, {2}})) // Error!Required for: Min, Max functions (not methods)
// Works: int is Ordered
glinq.Min(glinq.From([]int{1, 2, 3}))
// Use method with comparator for custom types
glinq.From(people).Min(func(a, b Person) int { return a.Age - b.Age })Required for: Sum
// Works: int is Numeric
glinq.Sum(glinq.From([]int{1, 2, 3}))
// Doesn't work: string is not Numeric
glinq.Sum(glinq.From([]string{"a", "b"})) // Error!result := glinq.From(data).
Where(func(x T) bool { /* filter */ }).
Select(func(x T) T { /* transform */ }).
ToSlice()top5 := glinq.From(data).
OrderByDescending(comparator).
Take(5).
ToSlice()// Using ToMapBy
groups := glinq.ToMapBy(
glinq.From(items),
func(item Item) string { return item.Category },
func(item Item) Item { return item },
)nested := [][]int{{1, 2}, {3, 4}, {5}}
flat := glinq.From(nested).
Aggregate([]int{}, func(acc []int, x []int) []int {
return append(acc, x...)
})Problem: Trying to use Distinct, Union, etc. with non-comparable types.
Solution: Use DistinctBy or ensure your type is comparable.
// Error
glinq.Distinct(glinq.From([][]int{{1}}))
// Fix
glinq.From([][]int{{1}}).
DistinctBy(func(x []int) any { return len(x) })Problem: Using Min/Max functions with non-ordered types.
Solution: Use Min/Max methods with comparator.
// Error
glinq.Min(glinq.From([]Person{{Age: 30}}))
// Fix
glinq.From([]Person{{Age: 30}}).
Min(func(a, b Person) int { return a.Age - b.Age })Problem: Slow operations on large datasets.
Solutions:
- Use
Takeearly to limit processing - Avoid
OrderByon very large streams - Consider materializing once if iterating multiple times
See CONTRIBUTING.md for guidelines.
MIT