# Pattern Matching in Go: How GALA Brings Sealed Types and Exhaustive Matching to the Go Ecosystem

Go is a language built on simplicity. But simplicity has costs, and one cost Go developers feel acutely is the lack of pattern matching and algebraic data types. If you have ever modeled a closed set of variants -- shapes, AST nodes, API responses, state machine states -- you know the pain: interfaces, type switches, zero exhaustiveness checking, and boilerplate that grows with every new variant.

GALA is a programming language that transpiles to Go, bringing sealed types, exhaustive pattern matching, and algebraic data types to the Go ecosystem with zero runtime overhead. The output is plain Go code. Every Go library works. Every Go tool works. You just get better type safety where it matters.

This post walks through the problem, the solution, and real code you can try today.

* * *

## The Problem: Modeling Variants in Go

Suppose you are building a graphics library and need to represent shapes. In Go, the standard approach uses interfaces and type switches:

```go
type Shape interface {
    isShape()
}

type Circle struct {
    Radius float64
}
func (c Circle) isShape() {}

type Rectangle struct {
    Width  float64
    Height float64
}
func (r Rectangle) isShape() {}

type Triangle struct {
    Base   float64
    Height float64
}
func (t Triangle) isShape() {}
```

To compute the area, you write a type switch:

```go
func area(s Shape) string {
    switch v := s.(type) {
    case Circle:
        return fmt.Sprintf("circle area: %.2f", math.Pi*v.Radius*v.Radius)
    case Rectangle:
        return fmt.Sprintf("rect area: %.2f", v.Width*v.Height)
    case Triangle:
        return fmt.Sprintf("triangle area: %.2f", 0.5*v.Base*v.Height)
    default:
        return "unknown shape"
    }
}
```

This works. But it has three problems that get worse as your codebase grows:

1.  **No exhaustiveness checking.** Add a new `Pentagon` variant. The compiler says nothing. The `default` branch silently absorbs it, or worse, you forget the `default` and get a zero-value return. You find the bug in production.
    
2.  **Boilerplate.** Every variant needs a struct, a marker method, and the type switch needs a `default` case. The interface itself (`isShape()`) carries no information -- it exists only to close the type set, and it does not even do that reliably since any package can implement it.
    
3.  **No destructuring.** You cannot extract fields inline. You match the type, then access fields on the bound variable. This is verbose and error-prone when variants have many fields.
    

These issues have been discussed in Go proposals for years ([golang/go#19412](https://github.com/golang/go/issues/19412), [golang/go#41716](https://github.com/golang/go/issues/41716)). Sum types and pattern matching remain among the most requested features. The Go team has chosen not to add them, and that is a reasonable decision for the language's goals. But the need does not go away.

* * *

## Enter GALA

[GALA](https://github.com/martianoff/gala) (Go Alternative Language) transpiles to Go. You write GALA source, the transpiler produces standard Go code, and you build the result with `go build` or Bazel. There is no runtime library, no reflection magic, no code generation at runtime. The generated Go is the kind of code you would write by hand -- just more of it, and correct.

GALA gives you sealed types (algebraic data types), exhaustive pattern matching with destructuring and guards, `Option[T]`/`Either[A,B]`/`Try[T]` monads, immutable collections, and full Go interop. You can import any Go package, call any Go function, and use any Go library.

The trade-off is honest: you add a transpilation step. Your source files are `.gala` instead of `.go`. IDE support is available for IntelliJ but not yet universal. Whether that trade-off is worth it depends on your project. For codebases heavy on variant types, state machines, or functional data processing, it often is.

* * *

## Sealed Types: Closed Type Hierarchies Done Right

Here is the same Shape example in GALA:

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

That is the entire definition. The transpiler generates:

*   A parent struct `Shape` with a `_variant` discriminator field
    
*   Companion objects `Circle{}`, `Rectangle{}`, `Triangle{}` with `Apply` methods for construction
    
*   `Unapply` methods for pattern matching (destructuring)
    
*   `IsCircle()`, `IsRectangle()`, `IsTriangle()` discriminator methods
    
*   `Copy` and `Equal` methods
    

Construction is concise:

```gala
val c = Circle(3.14)
val r = Rectangle(10.0, 20.0)
```

Sealed types also support generics:

```gala
sealed type Result[T any] {
    case Ok(Value T)
    case Err(Error error)
}

val success = Ok(42)
val failure = Err[int](fmt.Errorf("not found"))
```

Compare this to the Go version. The GALA definition is 4 lines. The Go version is 18 lines of structs and marker methods, and it still does not give you exhaustiveness checking or destructuring.

* * *

## Pattern Matching: Beyond the Type Switch

GALA's `match` expression supports literal matching, variable binding, destructuring, guards, nested patterns, and type-based matching. Here is the area function:

```gala
func area(s Shape) string = s match {
    case Circle(r)       => f"circle area: ${3.14159 * r * r}%.2f"
    case Rectangle(w, h) => f"rect area: ${w * h}%.2f"
    case Triangle(b, h)  => f"triangle area: ${0.5 * b * h}%.2f"
}
```

Notice what is different from Go:

*   **Destructuring.** `case Circle(r)` binds the `Radius` field to `r` in one step. No `v := s.(Circle); r := v.Radius`.
    
*   **Expression syntax.** The entire match is an expression that returns a value. No `var result string` declaration before the switch.
    
*   **No default case.** All three variants are covered. The compiler knows this. If you remove the `Triangle` branch, you get a compile error.
    

### Guards

Guards add conditions to patterns:

```gala
struct Person(Name string, Age int)

val status = person match {
    case Person(name, age) if age < 18 => s"$name is a minor"
    case Person(name, age) if age > 65 => s"$name is a senior"
    case Person(name, _)               => s"$name is an adult"
}
```

The equivalent Go code needs a type switch (or if-else chain), manual field access, and a mutable variable to hold the result:

```go
var status string
switch {
case person.Age < 18:
    status = fmt.Sprintf("%s is a minor", person.Name)
case person.Age > 65:
    status = fmt.Sprintf("%s is a senior", person.Name)
default:
    status = fmt.Sprintf("%s is an adult", person.Name)
}
```

### Nested Patterns

Patterns compose. You can match extractors inside extractors:

```gala
type Even struct {}
func (e Even) Unapply(i int) Option[int] = if (i % 2 == 0) Some(i) else None[int]()

val opt = Some(10)
opt match {
    case Some(Even(n)) => Println(s"even number: $n")
    case Some(n)       => Println(s"odd number: $n")
    case None()        => Println("nothing")
}
```

Try writing that with Go type switches. You would need nested switches, temporary variables, and manual extractor logic. In GALA, the intent is clear in a single expression.

### Type-Based Matching

When working with `any` or interface types, GALA supports type patterns directly:

```gala
val x any = "hello"

val res = x match {
    case s: string => "string: " + s
    case i: int    => s"int: $i"
    case _         => "unknown"
}
```

This is similar to Go's type switch, but integrated into the same match syntax as everything else.

* * *

## Exhaustive Matching: The Compiler Catches Missing Cases

This is where golang sealed types and exhaustive matching really pay off. When you match on a sealed type and miss a variant, the GALA transpiler rejects the code at compile time.

Given this sealed type:

```gala
sealed type TrafficLight {
    case Red()
    case Yellow()
    case Green()
}
```

This code compiles:

```gala
val action = light match {
    case Red()    => "stop"
    case Yellow() => "caution"
    case Green()  => "go"
}
```

But remove the `Yellow` case and the transpiler reports an error: the match is not exhaustive. You cannot forget to handle a variant.

In Go, you would use `iota` constants or separate types with a type switch, and nothing stops you from forgetting a case. Linters like `exhaustive` exist but they are optional, require configuration, and only work with `iota`\-based enums. GALA's exhaustiveness checking is built into the language and works with full algebraic data types, not just integer constants.

If you only care about some variants, use a wildcard catch-all:

```gala
val msg = light match {
    case Red() => "must stop"
    case _     => "can proceed"
}
```

This is explicit -- you are telling the compiler you have intentionally handled the remaining cases together.

* * *

## Real-World Example: Expression Evaluator

Let us build a simple arithmetic expression evaluator using sealed types and pattern matching. This is a classic use case for go algebraic data types.

```gala
package main

sealed type Expr {
    case Num(Value float64)
    case Add(Left Expr, Right Expr)
    case Mul(Left Expr, Right Expr)
}

func eval(e Expr) float64 = e match {
    case Num(v)    => v
    case Add(l, r) => eval(l) + eval(r)
    case Mul(l, r) => eval(l) * eval(r)
}

func show(e Expr) string = e match {
    case Num(v)    => f"$v%.0f"
    case Add(l, r) => s"(${show(l)} + ${show(r)})"
    case Mul(l, r) => s"(${show(l)} * ${show(r)})"
}

func main() {
    // (2 + 3) * 4
    val expr = Mul(Add(Num(2), Num(3)), Num(4))
    Println(s"${show(expr)} = ${eval(expr)}")
}
```

Output: `((2 + 3) * 4) = 20`

Now here is the equivalent Go code. It is what the transpiler generates (simplified for clarity):

```go
package main

import "fmt"

const (
    Expr_Num uint8 = iota
    Expr_Add
    Expr_Mul
)

type Expr struct {
    _variant uint8
    Value    float64
    Left     Expr
    Right    Expr
}

// Companion objects + Apply methods omitted for brevity

func eval(e Expr) float64 {
    switch e._variant {
    case Expr_Num:
        return e.Value
    case Expr_Add:
        return eval(e.Left) + eval(e.Right)
    case Expr_Mul:
        return eval(e.Left) * eval(e.Right)
    default:
        panic("non-exhaustive match")
    }
}

func show(e Expr) string {
    switch e._variant {
    case Expr_Num:
        return fmt.Sprintf("%.0f", e.Value)
    case Expr_Add:
        return fmt.Sprintf("(%s + %s)", show(e.Left), show(e.Right))
    case Expr_Mul:
        return fmt.Sprintf("(%s * %s)", show(e.Left), show(e.Right))
    default:
        panic("non-exhaustive match")
    }
}

func main() {
    expr := Expr{_variant: Expr_Mul,
        Left: Expr{_variant: Expr_Add,
            Left:  Expr{_variant: Expr_Num, Value: 2},
            Right: Expr{_variant: Expr_Num, Value: 3},
        },
        Right: Expr{_variant: Expr_Num, Value: 4},
    }
    fmt.Printf("%s = %.0f\n", show(expr), eval(expr))
}
```

The Go version is roughly three times longer. More importantly, the Go version has no compile-time guarantee that the switch is exhaustive. The `default: panic` is a runtime safety net, not a compile-time one. Add a `Div` variant to `Expr` and the Go compiler says nothing. The GALA compiler rejects the code until you handle `Div` in every match.

* * *

## Option, Either, and Try: Standard Library Patterns

GALA's standard library uses sealed types throughout. These are not special compiler features -- they are regular sealed types that you could define yourself:

```gala
sealed type Option[T any] {
    case Some(Value T)
    case None()
}

sealed type Either[A any, B any] {
    case Left(LeftValue A)
    case Right(RightValue B)
}

sealed type Try[T any] {
    case Success(Value T)
    case Failure(Err error)
}
```

They all support pattern matching:

```gala
val result = fetchUser(id) match {
    case Success(user) => s"Hello, ${user.Name}"
    case Failure(err)  => s"Error: $err"
}

val name = user.Email match {
    case Some(email) => email
    case None()      => "no email on file"
}
```

And they support monadic chaining with `Map`, `FlatMap`, `Filter`, `GetOrElse`, and `Recover`:

```gala
val displayName = user.Name
    .Map((n) => strings.ToUpper(n))
    .GetOrElse("ANONYMOUS")

val result = divide(10, 2)
    .Map((x) => x * 2)
    .FlatMap((x) => divide(x, 3))
    .Recover((e) => 0)
```

This eliminates entire categories of nil-pointer bugs and forgotten error checks. The types make missing cases visible, and pattern matching makes handling them concise.

* * *

## Try It Yourself

**In your browser:** The [GALA Playground](https://gala-playground.fly.dev) lets you write and run GALA code with no installation.

**On your machine:** Download a binary from [GitHub Releases](https://github.com/martianoff/gala/releases) for Linux, macOS, or Windows. Then:

```bash
# Create a file
cat > main.gala << 'EOF'
package main

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

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

func main() {
    Println(describe(Circle(3.14)))
    Println(describe(Rectangle(10, 5)))
}
EOF

# Run it
gala run main.gala
```

For projects, GALA integrates with Bazel:

```python
load("//:gala.bzl", "gala_binary")

gala_binary(
    name = "myapp",
    src = "main.gala",
)
```

* * *

## Conclusion

GALA does not replace Go. It transpiles to Go. The output is standard Go code that uses Go's type system, Go's runtime, and Go's toolchain. There is no runtime dependency, no reflection overhead, and no magic.

What GALA adds is a layer of compile-time safety for a specific class of problems: modeling closed variant types and ensuring every case is handled. If your codebase has state machines, AST nodes, protocol messages, API response types, or any domain with a fixed set of variants, sealed types and exhaustive pattern matching eliminate an entire category of bugs that Go's type system cannot catch.

The trade-off is a transpilation step and a different source language. For many projects, that trade-off is not worth it -- Go's simplicity is its strength. But for projects where type safety at the variant level matters more than syntactic familiarity, GALA gives you go pattern matching that the language itself has not yet provided.

Check out the [GitHub repository](https://github.com/martianoff/gala) for the full language specification, or try the [playground](https://gala-playground.fly.dev) to experiment with sealed types and pattern matching right now.
