Golang Pattern Matching — Beyond Go’s Switch Statement

Go’s switch is limited to value comparison. Runtime pattern matching libraries like go-pattern-match try to fill the gap but lack compile-time safety and clean syntax. GALA’s match expression gives you structural destructuring, exhaustive checking, guard clauses, nested patterns, custom extractors, and sequence matching — all as expressions that return values, with errors caught at compile time.


Overview

The match expression takes a subject and tests it against a series of case branches. The first matching branch wins, and its body becomes the result. A case _ default is required unless the match is provably exhaustive (sealed types with all variants covered, or booleans with both true and false).

val result = x match {
    case 1 => "one"
    case 2 => "two"
    case _ => "other"
}

Every match branch is an expression. You can assign the result directly to a val, pass it to a function, or use it inline.


Literal and Variable Binding

Match against literal values or bind the subject to a new variable:

val result = x match {
    case 1 => "one"
    case 2 => "two"
    case n => s"Value is $n"   // n is bound to x
    case _ => "other"
}

Unused variable rule: All variables extracted in match patterns must be referenced in the branch body or guard. Use _ to discard values you do not need:

// ERROR: unused variable 'y' in match branch
case Some(y) => "has value"

// OK: use '_' to discard
case Some(_) => "has value"

// OK: variable is used in body
case Some(y) => s"got $y"

// OK: variable is used in guard
case Some(y) if y > 0 => "positive"

Struct Destructuring

Structs with public fields automatically generate Unapply methods, enabling positional extraction in match patterns:

struct Person(Name string, Age int)

val people = SliceOf(
    Person("Alice", 25),
    Person("Bob", 15),
    Person("Charlie", 70),
)

for _, p := range people {
    val status = p match {
        case Person(name, age) if age < 18 => name + " is a minor"
        case Person(name, age) if age > 65 => name + " is a senior"
        case Person(name, _)               => name + " is an adult"
        case _                             => "Unknown"
    }
    Println(status)
}

You can match on specific field values and ignore others with _:

val msg = p match {
    case Person(name, 30) => name + " is 30"
    case Person(_, age)   => s"Someone is $age"
    case _                => "Unknown"
}

Sealed Type Matching (Exhaustive)

When matching on a sealed type, the compiler knows every variant. If you cover them all, no case _ is required:

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 r=$r%.2f"
    case Rectangle(w, h) => f"rect $w%.0f x $h%.0f"
    case Point()         => "point"
}

Forget a variant and the compiler rejects your code.


Guard Clauses

Add if conditions after a pattern to refine the match. Guards have access to all variables extracted by the pattern:

val res = x match {
    case i: int if i > 100 => "Large integer"
    case i: int if i > 0   => "Positive integer"
    case Person(name, age) if age < 18 => name + " is a minor"
    case _ => "Other"
}

Guards are evaluated after the pattern matches. If the guard fails, the next case is tried.


Type-Based Matching

Match on the runtime type of a value. This is useful with any or interface types:

val x any = "hello"

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

Generic Type Matching

Match against specific instantiations of generic types:

type Wrap[T any] struct { Value T }

val w = Wrap[int](Value = 42)
val res = w match {
    case w: Wrap[int]    => s"Wrapped int: ${w.Value}"
    case w: Wrap[string] => "Wrapped string: " + w.Value
    case _               => "Other"
}

Wildcard Generic Type Matching

Use [_] to match any instantiation of a generic type:

type Wrap[T any] struct { Value T }
func (w Wrap[T]) GetValue() any = w.Value

val w = Wrap[string](Value = "hello")
val res = w match {
    case w1: Wrap[_] => s"Matched Wrap[_]: ${w1.GetValue()}"
    case _           => "Other"
}

Nested Patterns

Patterns compose. You can nest extractors inside extractors for deep matching:

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("Found some even number", n)
    case Some(n)       => Println("Found some odd number", n)
    case None()        => Println("Nothing found")
    case _             => Println("Other")
}

Nested patterns work to arbitrary depth. The transpiler chains the Unapply calls automatically.


Sequence Patterns

GALA supports Scala-like sequence pattern matching for collections that implement the Seq interface (such as Array and List from collection_immutable). Use ... to match remaining elements:

import . "martianoff/gala/collection_immutable"

val arr = ArrayOf(1, 2, 3, 4, 5)

// Extract head and capture tail
val res = arr match {
    case Array(head, tail...) => s"Head: $head, Tail size: ${tail.Size()}"
    case _ => "Empty"
}

Rest Pattern Variants

val list = ListOf("a", "b", "c", "d")

// Capture first two, bind rest
val res = list match {
    case List(first, second, rest...) => s"$first, $second, rest size: ${rest.Size()}"
    case _ => "Not enough elements"
}

// Check minimum length without capturing
val hasThree = list match {
    case List(_, _, _, _...) => "Has at least 3 elements"
    case _                   => "Less than 3 elements"
}

Custom Extractors (Unapply)

Any struct with an Unapply method can be used as a pattern. Extractors come in two forms:

Option-Returning Extractors (Value Extraction)

Return Option[T]Some means the pattern matched, None means it did not:

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

val number = 42
val description = number match {
    case Even(n) => s"$n is even"
    case _       => s"$number is odd"
}

Boolean-Returning Extractors (Guard Patterns)

Return bool for simple yes/no matching without value extraction:

type Positive struct {}
func (p Positive) Unapply(i int) bool = i > 0

Boolean Exhaustive Match

Matching on a boolean with both true and false branches is exhaustive — no case _ needed:

val desc = flag match {
    case true  => "enabled"
    case false => "disabled"
}

Comparison: GALA match vs Go switch

GALAGo
```gala val msg = shape match { case Circle(r) => f"r=$r%.1f" case Rectangle(w,h) => f"$w%.0fx$h%.0f" case Point() => "point" } ``` ```go var msg string switch shape._variant { case Shape_Circle: msg = fmt.Sprintf("r=%.1f", shape.Radius.Get()) case Shape_Rectangle: msg = fmt.Sprintf("%fx%f", shape.Width.Get(), shape.Height.Get()) case Shape_Point: msg = "point" } ```
Feature GALA match Go switch
Expression (returns a value) Yes No
Struct destructuring Yes No
Exhaustive checking Yes (sealed types, booleans) No
Guard clauses Yes (if after pattern) No (separate if inside case)
Nested patterns Yes No
Custom extractors Yes (Unapply) No
Sequence patterns Yes (head, tail…) No
Type matching Yes (case x: Type) Yes (case Type:)

Further Reading