ahmetb/go-linq v5: typed LINQ-style chaining for Go 1.27
.NET LINQ capabilities in Go
At a glance
- What is it?
- go-linq brings .NET-style query chains to Go slices, maps, strings and iterators. Version 5 drops the any-based API for generic methods, which makes it type-safe but raises the Go version floor to 1.27.
- Who is it for?
- Adopt go-linq v5 if you are already building on Go 1.27 and want Where, Select, GroupBy and Join chains that keep their element types at compile time. Do not adopt it if your toolchain is pinned below 1.27, or if you only need one or two transformations that a plain for loop expresses in three lines.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 9 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 27, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What go-linq v5 solves, and who it is for
Go has no query syntax. Filtering a slice, projecting a field, grouping by a key and joining two collections are all hand-written loops, and each loop carries its own index variable, append call and accumulator. go-linq packages those operations as chainable methods on a Query[T] type, so a pipeline reads as a sequence of named steps rather than as control flow.
The audience is narrow but real. If you are porting .NET code, or you have Go code where the same three-level loop pattern appears in twenty functions, the library gives those loops a shared vocabulary. It is not a general-purpose data framework. There is no query planner, no lazy I/O layer, and no parallelism. The README describes it as a LINQ library for Go and lists the supported sources: slices, maps, strings, channels, iter.Seq[T] iterators and custom collections.
Generic methods are the mechanism behind the typed API
The v5 rewrite exists because of one language feature. Go 1.27 supports generic methods, meaning a method can declare its own type parameters, as in func (q Query[T]) Select[TResult any](...). Without that, a method on a query of T could not return a query of a different element type, which is exactly what Select, Join and GroupBy must do. Versions 1 through 4 worked around this by erasing everything to any and recovering the real types with runtime reflection.
The README shows the same query in both styles. In v4, the predicate is func(c any) bool and the body starts with a type assertion c.(Car). In v5, the predicate is func(c Car) bool and the selector returns a plain string. No type assertions appear in the chain, and the README states there is no any, no reflection and no type assertions in the typed path. Type parameters are inferred from the functions you pass, so call sites stay free of explicit type arguments.
Evaluation is lazy. The README describes the iterator pattern as being based on the standard iter.Seq[T] type, and the Query[T] type exposes an Iterate field of that type. That field is what makes a query usable in a native for ... range loop and compatible with the iter and slices standard library packages. The library also states it is safe for concurrent use.
One consequence of the typed design is visible in the constructor list. The v4 From(any) constructor, which used runtime reflection, has been removed. In a fully typed API the element type has to be known where the query starts, so you pick FromSlice, FromMap, FromChannel, FromChannelWithContext, FromString, FromSeq, Range or Repeat. FromMap returns a Query[KeyValue[TKey, TValue]] rather than a query of the raw map.
Installing go-linq v5 and running a first query
The README gives a single install line. The module path carries the v5 suffix, so the import and the get command both end in /v5.
go get github.com/ahmetb/go-linq/v5The go.mod in the repository declares go 1.27. That is not incidental: generic methods are the feature the whole typed API depends on. The README notes that with Go 1.26 or newer installed, the toolchain listed in go.mod is downloaded automatically, so the local toolchain does not have to be 1.27 itself. If you are on an older release, the README points to go-linq v4, which offers the same operators behind an any-based API.
A first query can start from any slice. The README's quickstart composes FromSlice, Where, Select and Union, and states that type parameters are inferred from your functions.
import . "github.com/ahmetb/go-linq/v5"
owners := FromSlice(cars).Where(func(c Car) bool {
return c.year >= 2015
}).Select(func(c Car) string {
return c.owner
}).ToSlice()ToSlice returns []string here, not []any. The element type that entered the chain is the element type that leaves it, and the compiler enforces that at each step.
The README's first full example is more involved: SelectMany flattens a slice of authors out of each book, GroupBy groups by author, and MaxBy picks the largest group, after which author.Key holds the name. A second example shows how to extend the library rather than just consume it. You declare a named type over Query[int] and add a method that returns a Query[int].
type MyQuery Query[int]
func (q MyQuery) GreaterThan(threshold int) Query[int] {
return Query[int](q).Where(func(item int) bool {
return item > threshold
})
}
result := MyQuery(Range(1, 10)).GreaterThan(5).ToSlice()That pattern is the practical reason to prefer this library over a set of free functions: your own domain-specific operators compose with the built-in ones.
The Go 1.27 requirement is the main adoption constraint
The requirement section is blunt: go-linq v5 requires Go 1.27, described there as currently in release candidate. Teams that pin a toolchain, ship against an older language version, or maintain a library that must compile on Go 1.26 cannot use v5 at all. They can use v4, but v4 is a different API with any in every signature, so the migration is not a version bump.
The performance picture has a similar shape. The README reports v5 as 5 to 15 times faster than idiomatic v4 and 25 to 77 times faster than the v4 reflection API, with allocations dropping from O(n) to O(1) per query. Those numbers come from a specific setup: an Apple M5 Pro, go1.27, a 1M-element []int, and 100k structs for the projection case. The same table shows the hand-written loop still ahead, at 0.45 ms against 1.5 ms for Where followed by Sum. The README says as much, noting the residual gap to a hand-written loop.
So the honest framing is that go-linq removes the abstraction penalty relative to its own predecessors, not relative to writing the loop yourself. If a hot path is a single Where over a large slice, the loop is still faster and has no dependency. The library pays off when the chain is long enough that the loop version becomes hard to read or hard to change.
Two smaller boundaries are worth noting. There is no From(any), so code that built queries from dynamically typed values has to be rewritten around a concrete source constructor. And the README documents the operators and constructors but does not discuss error handling or cancellation beyond the existence of FromChannelWithContext, so anyone querying a channel should read that function's documentation on godoc rather than assume a policy.
How go-linq compares with samber/lo and plain loops
The alternative people most often reach for is samber/lo, which appears in the related searches alongside the phrase Go LINQ equivalent. The difference is structural rather than cosmetic. lo is a collection of generic helper functions: you call Filter, Map or Uniq on a slice and get a slice back, one call at a time. There is no query object and no chain state. go-linq builds a Query[T] that carries an iter.Seq[T] iterator, so operations stack lazily and nothing is computed until a terminal call such as ToSlice, MaxBy or a range over Iterate.
That laziness changes what you can express. A go-linq chain can GroupBy, then OrderByDescending, then ThenBy, then Take, then SelectIndexed, which is what the README's MapReduce example does to produce the top five words across a set of sentences. Reproducing that with standalone helpers means materializing intermediate slices between each step. The trade-off runs the other way too: a single Filter call in lo is easier to read than FromSlice(x).Where(...).ToSlice(), and it has no query construction to reason about.
The third option is no library. A for loop with an append is the fastest thing in the README's own table and needs no dependency, no Go 1.27, and no migration file. go-linq is worth its weight when the same chained shape repeats across a codebase, not when it appears once.
Version 5 upgrade cost and licence terms
The repository ships a MIGRATION.md at the top level next to README.md, which is where the v4 to v5 changes are documented. The visible breaking change is the removal of the reflection-based From(any) constructor and the shift of every operator signature from any to a concrete type parameter. The README frames this as the point of the release rather than a side effect: the any-based API exists only because generic methods were unavailable.
A migration is therefore mostly mechanical but not automatic. Every predicate and selector loses its type assertion, and every query needs a typed constructor at its head. The README's side-by-side comparison of the same query in v4 and v5 is the best template for that edit. Release history shows v3.2.0 in 2020, v4.0.0 in 2025 and v5.0.0 in 2026, so major versions are years apart and the v5 line is the current one; the last push to the repository was on 2026-09-20.
The licence is Apache-2.0, listed in the repository root as LICENSE. Apache-2.0 is a permissive licence that includes an explicit patent grant from contributors and requires that notices and the licence text be preserved in redistributed copies. If you vendor the source or ship it inside a product, keep the LICENSE file and any NOTICE-style attribution intact. That is a description of the licence terms, not legal advice; get your own review if the distribution model is unusual.
Editorial conclusion
Adopt go-linq v5 if you are already building on Go 1.27 and want Where, Select, GroupBy and Join chains that keep their element types at compile time. Do not adopt it if your toolchain is pinned below 1.27, or if you only need one or two transformations that a plain for loop expresses in three lines. Before committing, read MIGRATION.md for the v4 to v5 changes, confirm that the From(any) constructor your code uses is gone, and check the Performance table in the README against your own element sizes, since the residual gap to a hand-written loop is not zero.
Frequently asked questions
What does go-linq v5 require to build?
It requires Go 1.27, because type-changing operators such as Select, Join and GroupBy are implemented as generic methods, a Go 1.27 language feature. The go.mod file declares go 1.27, and the README notes that with Go 1.26 or newer installed the toolchain from go.mod is downloaded automatically. For older Go versions the README points to go-linq v4.
How do I install go-linq v5?
The README gives one command, go get github.com/ahmetb/go-linq/v5, and the module path includes the /v5 suffix. Imports use the same path, and the quickstart example imports it with a dot import so that FromSlice and the other constructors are available unqualified.
What is the difference between go-linq v4 and v5?
v4 erases element types to any and uses runtime reflection, so predicates and selectors contain type assertions. v5 uses generic methods so the element type flows through the chain at compile time, with no any, no type assertions and no reflection. The reflection-based From(any) constructor was removed in v5, and migration notes live in MIGRATION.md.
Which data sources can a go-linq query read from?
The README lists FromSlice, FromMap, FromChannel, FromChannelWithContext, FromString, FromSeq, Range and Repeat. FromMap produces a Query[KeyValue[TKey, TValue]], FromString produces a Query[rune], and FromSeq accepts any standard iter.Seq[T] iterator, including custom collections that expose an iterator method.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/ahmetb-go-linq)