GALA transpiles to Go. Every Go package, type, and function is available in GALA code with zero friction. Your existing Go modules, third-party libraries, and tooling all work out of the box.
This page covers calling Go from GALA, then the other direction in brief. For packages that mix .gala and .go files, project layout, building and the gotchas, see the Go Interop Guide.
Standard Go imports work directly in GALA:
import "strings"
import "fmt"
import "net/http"
import "encoding/json"
Grouped imports are supported:
import (
"strings"
"net/http"
"os"
)
Aliased and dot imports work too:
import mystrings "strings"
import . "martianoff/gala/std"
Call any Go function with GALA’s syntax:
import "strings"
val upper = strings.ToUpper("hello") // "HELLO"
val parts = strings.Split("a,b,c", ",") // []string{"a","b","c"}
val contains = strings.Contains("hello", "ell") // true
Many Go functions return more than one result — usually a value plus an
error that says whether the call worked. In GALA every expression is one
value, so such a call becomes one GALA value automatically:
| The Go function returns | In GALA the call is a | Example | Its GALA value |
|---|---|---|---|
a value and an error (T, error) |
Try[T] |
strconv.Atoi(s) |
Try[int] |
| several values and an error | Try of a Tuple |
net.SplitHostPort(addr) |
Try[Tuple[string, string]] |
| two or more values | a Tuple | strings.Cut(s, "=") |
Tuple3[string, string, bool] |
only an error |
the error itself |
os.Remove(path) |
error (nil when it worked) |
A Try is Success(value) when the call worked and Failure(err) when it
returned an error, so you match on it or use its methods directly:
import (
"os"
"strconv"
"strings"
)
val port = strconv.Atoi("8080") match {
case Success(n) => n
case Failure(_) => 80
}
val dir = os.Getwd().GetOrElse("/tmp")
val (key, value, found) = strings.Cut("mode=fast", "=")
Using the Try where the plain value is expected — val n = strconv.Atoi(s)
and then n + 1 — is a compile-time error,
GALA-E0049. Take the value with .Get() (panics
on failure), .GetOrElse(default), or a match.
A Go function that returns only an error gives you that error. Wrap the call
in Try(...) to turn a non-nil error into a Failure: Try(os.Remove(path))
is a Try[Void]. Around a (T, error) call, Try(...) gives the same
Try[T] and additionally catches a panic.
To get Go’s results one by one — for example to hand them straight back to other Go code — bind several names:
import "os"
val file, err = os.Open("data.txt") // file *os.File, err error
Third-party modules work exactly like the standard library — not just stdlib.
Add the module as a Go dependency, import it, and call it directly. GALA reads
the module’s source to infer concrete types (return types, struct fields,
methods), so you get uuid.UUID, never any.
Add the dependency with --go so GALA tracks it as a Go (not GALA) package:
gala mod add github.com/google/uuid@v1.6.0 --go
Then use it like any other package:
import "github.com/google/uuid"
func main() {
val id = uuid.New()
Println(s"id: ${id.String()}")
}
A function returning (T, error) — like uuid.Parse(string) (uuid.UUID, error)
— works exactly like a standard-library one: the call is a Try[uuid.UUID]
you can match on directly:
import "github.com/google/uuid"
func describe(s string) string = uuid.Parse(s) match {
case Success(id) => s"valid: ${id.String()}"
case Failure(_) => s"invalid: $s"
}
Both the plain CLI (gala build / gala run) and Bazel builds resolve these
types from the dependency’s source. See
Dependency Management for adding and pinning Go
modules.
Define and use Go struct types with GALA syntax:
import "net/http"
// Go struct types work in GALA
val client = &http.Client{}
GALA structs are Go structs under the hood, so they interoperate seamlessly:
import "encoding/json"
type Config struct {
var Host string
var Port int
}
// Pass to any Go function expecting this struct; a (T, error) result comes back as a Try
val jsonBytes = json.Marshal(Config(Host = "localhost", Port = 8080)) // Try[[]byte]
GALA supports Go-style type conversions:
// Numeric conversions
val n = int64(42)
val f = float64(10)
val pi = 3.14
val i = int(pi) // truncates to 3
// Rune/string conversions
val r = rune(65) // int to rune: 'A'
val s = string(r) // rune to string: "A"
For byte and rune slice conversions, use the go_interop helpers:
import . "martianoff/gala/go_interop"
val bytes = ToBytes("hello") // string to []byte
val str = ToString(bytes) // []byte to string
val runes = ToRunes("hello") // string to []rune
[]T InteropGALA collections (Array, List) are preferred for general programming. When you need Go’s native []T — for passing data to Go libraries or variadic arguments — use the go_interop package:
import . "martianoff/gala/go_interop"
val goSlice = SliceOf(1, 2, 3, 4, 5) // Go []int
val empty = SliceEmpty[int]() // Go []int{}
val withCap = SliceWithCapacity[int](10) // []int with capacity 10
Convert between GALA collections and Go slices:
import . "martianoff/gala/collection_immutable"
import . "martianoff/gala/go_interop"
// GALA Array → Go slice
val arr = ArrayOf(1, 2, 3)
val goSlice = arr.ToGoSlice()
// Go slice → GALA Array
val backToArray = ArrayFromSlice(goSlice)
| Use Case | Recommendation |
|---|---|
| General programming | Array or List from collection_immutable |
| Need Map/Filter/FoldLeft | Array or List (full functional API) |
| Passing data to Go libraries | SliceOf from go_interop, or .ToGoSlice() |
| Variadic function arguments | Go slices with SliceOf |
| Function | Description |
|---|---|
SliceOf(elements...) |
Create Go slice from values |
SliceEmpty[T]() |
Create empty Go slice |
SliceWithCapacity[T](cap) |
Empty slice with capacity |
SliceCopy(slice) |
Copy a slice |
SliceClip(s) |
Drop unused capacity, so appending always reallocates |
SliceSortStable(s, cmp) |
Stable in-place sort; cmp(i, j) compares s[i] and s[j] |
SliceSortedStable(s, cmp) |
Stably sorted copy, leaving s unchanged |
SliceAppendAll(dst, src) |
Append all elements |
SlicePrepend(s, value) |
Insert at front |
SliceTake(s, n) |
Take first n elements |
SliceDrop(s, n) |
Drop first n elements |
map[K]V InteropGALA’s HashMap is preferred for most use cases. When you need Go-native map[K]V for interoperability:
import . "martianoff/gala/go_interop"
// Create and populate
var goMap = MapEmpty[string, int]()
goMap = MapPut(goMap, "key", 42)
// Query
val (value, ok) = MapGet(goMap, "key")
val exists = MapContains(goMap, "key")
// Iterate
MapForEach(goMap, (k string, v int) => {
Println(s"$k: $v")
})
Convert between HashMap and Go map:
import "martianoff/gala/collection_immutable"
val hashMap = collection_immutable.HashMapFromGoMap(goMap)
val backToGoMap = hashMap.ToGoMap()
| Function | Description |
|---|---|
MapEmpty[K, V]() |
Create empty map |
MapPut(m, k, v) |
Add/update entry, returns map |
MapGet(m, k) |
Get value and existence flag |
MapContains(m, k) |
Check if key exists |
MapDelete(m, k) |
Remove key, returns map |
MapLen(m) |
Number of entries |
MapForEach(m, f) |
Iterate all entries |
Bare Go builtins such as len, cap, make and panic are not part of GALA’s surface — calling one is GALA-E0035. Each has a GALA form or a go_interop wrapper:
import "martianoff/gala/go_interop"
val length = "hello".Size() // 5 — characters; .ByteSize() for bytes
val mySlice = go_interop.SliceOf(1, 2, 3)
val sliceCap = go_interop.SliceCap(mySlice) // slice capacity
// Println and Print are available without importing fmt
Println("hello world")
Print("no newline")
The full table of builtins and their replacements is in the language reference.
A GALA package is a Go package after transpilation, so Go code imports it by its import path. The generated Go is shaped for Go callers.
A GALA type implements a Go interface by having its methods. A method that Go calls with several results, such as io.Writer’s Write([]byte) (int, error), declares Go’s result list and gives the results as one GALA value: a Try for (T, error), a Tuple for (A, B). An interface literal implements an interface inline, one function per method, with no struct:
package main
import "fmt"
struct WriteCounter(var Writes int)
// io.Writer: the method declares Go's result list and gives it as one Try.
func (c Ptr[WriteCounter]) Write(p []byte) (int, error) {
c.Writes = c.Writes + 1
Success(p.Size())
}
func main() {
var counter = WriteCounter(0)
fmt.Fprintln(&counter, "first")
fmt.Fprintln(&counter, "second")
Println(counter.Writes) // 2
// An interface literal implements a Go interface inline.
val greeting = fmt.Stringer(String = () => "hello")
Println(s"$greeting, ${greeting.String()}") // hello, hello
}
A lambda passed where Go expects a func() (T, error) callback gives a Try the same way. See Returning several results to Go and Interface Literals.
Go can build a struct with a composite literal, which skips the field defaults GALA declares. So an exported shorthand struct whose fields are all exported gets a NewT(...) constructor, an exported sealed type gets NewT() for its default case and NewT<Case>(...) for each case, and a field with a default becomes a With...(v) option. Each exported case also gets an Is<Case>() predicate. Given this GALA package:
struct LineItem(SKU string, UnitCents int64, Qty int = 1)
sealed type Discount {
default case NoDiscount()
case Percent(Pct int64 = 10)
case AmountOff(Cents int64)
}
Go code calls it like this (from a Go example test that CI runs):
discount := pricing.NewDiscountPercent(pricing.WithDiscountPercentPct(50))
fmt.Println(discount, discount.Apply(900))
fmt.Println(pricing.NewDiscountPercent(), pricing.NewDiscount())
item := pricing.NewLineItem("C-3", 475, pricing.WithLineItemQty(2))
fmt.Println(item.SKU.Get(), item.Qty.Get())
// Output:
// Percent(50) 450
// Percent(10) NoDiscount()
// C-3 2
A sealed value’s case is tested with its predicate, d.IsPercent(), and its fields are read with the case’s Unapply. Doc comments on GALA declarations are carried into the generated Go, so go doc, gopls and pkg.go.dev show them, along with notes on the generated constructors.
go.gala.fyi/stdlib and gala exportgo build does not transpile .gala files, so a plain Go module needs the generated Go. Every GALA release publishes the standard library as the Go module go.gala.fyi/stdlib at the same version, and gala export writes a GALA project as an ordinary Go module that requires it:
go get go.gala.fyi/stdlib@latest # GALA's collections, Option, Try for Go code
gala export --out ../shapes-go # a GALA library as a plain Go module
Publish the exported directory as a module, and Go programs use the library with go get, with no GALA toolchain installed. Exporting a project that imports another GALA module is not supported yet. The Go Interop Guide covers versions, publishing, and gala transpile --stdlib-module.
.gala and .go files, building with gala build and Bazel, and the gotchas