# samber/lo Has 21K Stars. Here's What It Would Look Like as a Language Feature.

samber/lo has 21,000 stars. samber/mo has 3,300. Together, they represent one of the loudest feature requests the Go community has ever filed -- not through proposals or GitHub issues, but through adoption. Tens of thousands of Go developers have looked at `for i, v := range slice` and said: there has to be a better way.

They are right. And lo and mo are genuinely good solutions within the constraints of Go's type system. But those constraints are real, and they shape every API in ways that become visible the moment you compare them to what a language-level implementation looks like.

I am the author of [GALA](https://github.com/martianoff/gala), a language that transpiles to Go. This is not a "lo is bad" post. This is a post about what happens when you move functional patterns from library space into language space -- and what you gain and lose in the process.

## What lo and mo solve

lo gives Go developers Lodash-style collection operations: Map, Filter, Reduce, GroupBy, Chunk, Flatten, and dozens more. Before generics landed in Go 1.18, this was impossible without code generation or `interface{}`. lo was one of the first libraries to prove that Go generics could deliver real ergonomic gains.

mo goes further. It brings monadic types to Go: `Option[T]`, `Result[T]`, `Either[L, R]`, `Future[T]`. These are types that encode success/failure, presence/absence, and either/or semantics directly in the type system, enabling method chaining instead of `if err != nil` waterfalls.

Both libraries are well-designed. Both solve real problems. But both also bump into limitations that no Go library can work around.

## Side by side: Collection operations

Here is a common pattern -- filter a list of users, extract names, aggregate a result.

**samber/lo:**

```go
evens := lo.Filter(users, func(u User, _ int) bool {
    return u.Active && u.Age >= 18
})

names := lo.Map(evens, func(u User, _ int) string {
    return u.Name
})

result := strings.Join(names, ", ")

totalAge := lo.Reduce(users, func(acc int, u User, _ int) int {
    return acc + u.Age
}, 0)
```

**GALA:**

```gala
val result = users
    .Filter((u) => u.Active && u.Age >= 18)
    .Map((u) => u.Name)
    .MkString(", ")

val totalAge = users.FoldLeft(0, (acc, u) => acc + u.Age)
```

Three differences stand out.

**The unused index parameter.** Every lo callback takes an extra `_ int` index parameter, even when you do not need it. This is a design compromise -- lo provides a single API for both indexed and non-indexed access. In GALA, lambdas take only the parameters the operation requires.

**Method chaining vs function wrapping.** lo uses free functions: `lo.Filter(lo.Map(xs, f), g)`. To chain operations, you nest calls or use intermediate variables. GALA collections are types with methods, so operations chain naturally: `xs.Map(f).Filter(g)`. The pipeline reads left to right, top to bottom.

**Lambda syntax.** Go requires `func`, explicit parameter types, explicit return type, and curly braces. GALA infers parameter types from the method signature: `(u) => u.Name` instead of `func(u User, _ int) string { return u.Name }`. The type information is still checked -- you just do not have to write it.

Here is a numeric example to make the contrast sharper:

**samber/lo:**

```go
evens := lo.Filter(nums, func(x int, _ int) bool {
    return x%2 == 0
})
squares := lo.Map(evens, func(x int, _ int) int {
    return x * x
})
sum := lo.Reduce(squares, func(acc int, x int, _ int) int {
    return acc + x
}, 0)
```

**GALA:**

```gala
val result = nums
    .Filter((x) => x % 2 == 0)
    .Map((x) => x * x)
    .FoldLeft(0, (acc, x) => acc + x)
```

Same logic. One reads as a pipeline. The other reads as a sequence of transformations with ceremony around each step.

## Side by side: Option types

mo brings `Option[T]` to Go, which is a genuine improvement over nil pointers.

**samber/mo:**

```go
opt := mo.Some(42)
result := opt.FlatMap(func(value int) mo.Option[int] {
    if value > 50 {
        return mo.Some(value * 2)
    }
    return mo.None[int]()
}).OrElse(0)
```

**GALA:**

```gala
val result = Some(42)
    .Map((x) => x * 2)
    .FlatMap((x int) => if (x > 50) Some(x) else None[int]())
    .GetOrElse(0)
```

The API shapes are similar -- both support Map, FlatMap, and a default value fallback. But GALA's version benefits from shorter lambda syntax and the ability to use `if/else` as an expression (it returns a value, so no curly braces or explicit return needed).

The real difference shows up when you pattern match on the result:

**samber/mo:**

```go
either := mo.Right[string, int](42)
either.Match(
    func(err string) { fmt.Println("error:", err) },
    func(val int) { fmt.Println("value:", val) },
)
```

**GALA:**

```gala
val r1 = validateAge(25) match {
    case Right(age) => s"Valid age: $age"
    case Left(err)  => s"Error: $err"
}
```

mo's `Match` takes two function callbacks. GALA's `match` is a language construct that destructures the value, binds variables, and returns a result -- all in one expression. The GALA version is also exhaustive: if `Either` had a third variant, the compiler would reject incomplete matches.

## The three things Go lacks that no library can fix

lo and mo are pushing against the boundaries of what Go's type system allows. Three specific limitations shape every API decision:

**1\. Lambda syntax.** Go's anonymous functions require `func`, parameter types, return type, and braces. This is fine for callbacks that span multiple lines, but painful for one-liners. `func(x int, _ int) bool { return x%2 == 0 }` is a lot of syntax for "is this number even?" GALA's `(x) => x % 2 == 0` says the same thing with less noise, because parameter types are inferred from context.

**2\. Type inference for generics.** Go can infer type parameters in some cases, but not all. mo requires `mo.None[int]()` -- you cannot write `mo.None()` and have the compiler figure out `T=int` from context. GALA's type inference resolves generic parameters from usage: `Some(42)` is `Option[int]` without annotation. `FoldLeft(0, (acc, x) => acc + x)` infers that the accumulator is `int` from the zero value.

**3\. Algebraic data types.** This is the big one. Go has no sum types. mo implements Option as a struct with a boolean flag and a value field. Either is a struct with a flag indicating left or right. These work, but the compiler cannot enforce exhaustive handling. You can forget to check `IsPresent()` before calling `MustGet()`, and the compiler will not stop you. GALA's sealed types are true algebraic data types with compiler-enforced exhaustiveness:

```gala
sealed type Shape {
    case Circle(Radius float64)
    case Rectangle(Width float64, Height float64)
    case Point()
}

func describe(s Shape) string = s match {
    case Circle(r)       => f"circle with radius=$r%.2f"
    case Rectangle(w, h) => s"rectangle ${w}x${h}"
    case Point()         => "a point"
}
```

Add a new variant to `Shape` and every match expression that does not handle it becomes a compile error. No library in Go can provide this guarantee, because Go's type system does not support closed type hierarchies.

## What GALA adds beyond lo and mo

Beyond cleaner syntax for the same operations, GALA provides constructs that have no library equivalent:

**Immutability by default.** Variables declared with `val` cannot be reassigned. Struct fields are immutable unless explicitly marked `var`. Every struct gets an auto-generated `Copy` method for creating modified versions. This is not a convention -- it is compiler-enforced.

**String interpolation.** `s"Hello $name"` and `f"$value%.2f"` replace `fmt.Sprintf` calls. Format verbs are inferred from the variable type. No import needed.

**Expression-oriented syntax.** `if/else`, `match`, and single-expression functions all return values. `func square(x int) int = x * x` -- no braces, no return keyword. This removes an entire category of "forgot to return" bugs.

**Full Go interop.** GALA transpiles to Go. Every Go package is importable. Every Go type is usable. There is no FFI boundary, no wrapper generation, no runtime overhead beyond what the transpiler generates. If you use `net/http`, `database/sql`, or any Go library, it works directly.

## When to use lo/mo vs GALA

**Use lo/mo when:**

*   You have an existing Go codebase and want incremental improvements
    
*   Your team is comfortable with Go and does not want to learn a new syntax
    
*   You need a few specific utilities (lo's `Chunk`, `GroupBy`, `Uniq`) without adopting a new language
    
*   You want to stay within the official Go toolchain
    

**Consider GALA when:**

*   You are starting a new Go-targeted project and want stronger type guarantees from day one
    
*   Your domain has complex state machines, protocol types, or AST-like structures that benefit from sealed types
    
*   You want immutability enforced by the compiler, not by convention
    
*   You come from Scala, Kotlin, or Rust and want similar expressiveness with Go's deployment model
    
*   You are building data transformation pipelines where functional collection operations dominate the code
    

## Honest tradeoffs

GALA is not a free upgrade. Here is what you give up:

**Ecosystem maturity.** Go has world-class tooling: debuggers, profilers, IDE support, error messages refined over 15 years. GALA has an IntelliJ plugin with syntax highlighting and completion, but it is not at the same level. You will hit rough edges.

**Team familiarity.** lo is Go code. Any Go developer can read it, debug it, and contribute to it. GALA requires learning a new syntax. The concepts (sealed types, pattern matching, FoldLeft) are well-established in other languages, but they require investment from developers who have not used them before.

**Compilation step.** GALA adds a transpilation step before Go compilation. With Bazel, this is seamless. Without it, you are running `gala build` before `go build`.

**Community size.** lo has 21,000 stars and a large contributor base. GALA is a younger project. The standard library is solid -- Option, Either, Try, Future, six collection types -- but the ecosystem around it is smaller.

These are real costs. Whether the expressiveness gains justify them depends on your team, your domain, and your tolerance for early-adopter friction.

## Try it

GALA has a browser-based playground at [gala-playground.fly.dev](https://gala-playground.fly.dev) with built-in examples. No installation required.

For local development: download a binary from [GitHub releases](https://github.com/martianoff/gala/releases), or build from source with Bazel. GALA is at v0.17.1, with binaries for Linux, macOS, and Windows.

The 21,000 stars on samber/lo are not just a success story for a library. They are evidence that a significant segment of Go developers wants functional programming patterns -- and is willing to adopt third-party dependencies to get them. GALA asks: what if those patterns were not bolted on, but built in?

* * *

*I am the author of GALA. Source:* [*github.com/martianoff/gala*](https://github.com/martianoff/gala)
