# From Zero to a Go Module on Bazel in One Sitting: Onboarding to GALA 0.53

There's a specific moment that decides whether a new language is worth your time: the first time you try to use something from the ecosystem you already depend on. For a language that transpiles to Go, that means pulling in a third-party Go module and having it *just work* — concrete types, real build, no `interface{}` leaking through.

I wanted to know if GALA passes that test today, so I did the honest thing: started in an empty directory with nothing but the `gala` CLI, Go, and Bazel on my PATH, and tried to build a program that imports a Go module through Bazel and `rules_gala`. No copying an example repo, no insider shortcuts. This post is the actual transcript, friction and all.

The short version: it works, the types come out concrete, and the whole thing is about four small files you write by hand plus two generated ones. There's exactly one wiring detail the main docs get wrong, and I'll show you the version that actually works.

---

## The goal

A program that imports [`github.com/google/uuid`](https://github.com/google/uuid) — a real third-party Go module, not stdlib — generates a UUID, and prints it. Built and run entirely through Bazel with `rules_gala`. The interesting part isn't the UUID; it's whether GALA can read a third-party Go package's types well enough to produce concrete, compiling Go.

Versions used: `gala` 0.53.1, Go 1.25.5, Bazel 8.

---

## Step 1 — Initialize the module and add the Go dependency

```bash
gala mod init example.com/hellouuid
gala mod add github.com/google/uuid@v1.6.0 --go
```

```
Initialized gala.mod for module example.com/hellouuid
Fetching github.com/google/uuid@v1.6.0...
Added github.com/google/uuid@v1.6.0 (Go dependency)
```

The `--go` flag is the whole story of GALA's dependency model: it marks a requirement as a *Go* package — fetched and linked, never transpiled — as opposed to a GALA library, which is fetched as `.gala` source and transpiled at build time. The resulting `gala.mod` is deliberately Go-mod-shaped:

```
module example.com/hellouuid

gala 0.53.1

require github.com/google/uuid v1.6.0 // go
```

No `go.mod` in your project yet, and that's intentional — GALA projects are driven by `gala.mod`.

## Step 2 — Write the program

```gala
package main

import "github.com/google/uuid"

func main() {
    val id = uuid.New()
    Println(s"Fresh UUID: ${id.String()}")
}
```

That's the entire program. `uuid.New()` is a Go function returning a `uuid.UUID`; `id.String()` is a method on that Go type. `Println` and the `s"..."` interpolation are GALA built-ins — no `fmt` import needed. The question is whether the transpiler knows what `uuid.New()` returns. Hold that thought.

## Step 3 — The Bazel wiring

Four files. The `.bazelrc` selects the `rules_gala` registry and passes the Go SDK through to the transpiler:

```
common --enable_bzlmod
common --noenable_workspace
build  --enable_runfiles

common --registry=https://raw.githubusercontent.com/martianoff/rules-gala/main/
common --registry=https://bcr.bazel.build

build  --action_env=GOROOT
build  --action_env=PATH
```

`MODULE.bazel` is normal bzlmod — the GALA toolchain, Go rules, and the Gazelle extension:

```starlark
module(name = "hellouuid", version = "0.0.1")

bazel_dep(name = "rules_gala", version = "0.1.3")
bazel_dep(name = "gala", version = "0.53.1")
register_toolchains("@gala//tools/toolchain:gala_toolchain")

bazel_dep(name = "rules_go", version = "0.50.1")
bazel_dep(name = "gazelle", version = "0.39.1")
bazel_dep(name = "gala_gazelle", version = "0.2.5", dev_dependency = True)

go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
go_deps.from_file(go_mod = "//:go.mod")
use_repo(go_deps, "com_github_google_uuid")
```

And the root `BUILD.bazel` — **this is the one line that matters**:

```starlark
load("@gala_gazelle//gala:defs.bzl", "gala_gazelle")

gala_gazelle(name = "gazelle")
```

> **The one gotcha.** If you reach for the plain `gazelle(name = "gazelle")` rule
> from `@gazelle//:def.bzl` — the one you'd use in a pure-Go project — Gazelle
> only knows the Go language. It greets you with
> `unknown directive: gazelle:gala_prefix` and generates **zero** GALA targets.
> The fix is the `gala_gazelle` macro above: it builds a *composite* Gazelle
> binary that embeds both the Go language and the GALA language, plus a
> toolchain-driven import helper. Use it and BUILD generation covers Go, GALA,
> and mixed packages in one pass.

## Step 4 — Generate, build, run

`gala mod tidy` notices the project is now a Bazel project (because `MODULE.bazel` exists) and emits a portable `go.mod`/`go.sum` carrying just the Go dependencies:

```bash
gala mod tidy
```

Then Gazelle writes the build target for you:

```bash
bazel run //:gazelle
```

It appends a `gala_binary` to `BUILD.bazel`, with the Go dependency already resolved to its Bazel label — I didn't type this:

```starlark
gala_binary(
    name = "hellouuid",
    srcs = ["main.gala"],
    deps = ["@com_github_google_uuid//:uuid"],
)
```

Build and run:

```bash
bazel build //...
bazel run //:hellouuid
```

A real UUID, from a real Go module, through the full Bazel pipeline.

---

## The part that actually matters: the types are concrete

Anyone can make `interface{}` compile. The test of "Go interop" is whether the transpiler *understood* the third-party type. Here's the Go that GALA generated for `main.gala`:

```go
// Code generated by GALA transpiler. DO NOT EDIT.
package main

import "fmt"
import "martianoff/gala/std"
import "github.com/google/uuid"

func main() {
    var id = std.NewImmutable(uuid.New())
    fmt.Println(fmt.Sprintf("Fresh UUID: %v", id.Get().String()))
}
```

`uuid.New()` is called directly and its concrete `uuid.UUID` result flows into `id.Get().String()`. There is no `any`, no type assertion, no reflection. If the transpiler had failed to resolve `uuid.New()`'s return type, `id` would have been `any` and `.String()` would not compile — the build would have failed, not silently degraded. It compiled, so the type resolution is real.

This is the capability that landed recently: GALA reads the **Go SDK at transpile time** to resolve function signatures, struct fields, and method sets from third-party modules — not just stdlib. That's why the `.bazelrc` passes `GOROOT` and `PATH` into the build actions. It's also why `(T, error)` returns from Go functions get auto-wrapped into `Try[T]` at the call site: the transpiler can see the shape of what it's calling.

---

## Was it genuinely simple? An honest scorecard

Yes — with one caveat I won't paper over.

**What you write by hand:** `main.gala` (6 lines), `.bazelrc` (~8 lines), `MODULE.bazel` (~18 lines), `BUILD.bazel` (3 lines + a directive). **What's generated for you:** `go.mod`/`go.sum` (by `gala mod tidy`) and the `gala_binary` target (by Gazelle). The dependency label resolution — the part that's tedious and error-prone in hand-written Bazel — is automatic.

**The friction, named honestly:**

- **The Gazelle wiring.** The working recipe is the `gala_gazelle` macro from `@gala_gazelle//gala:defs.bzl`, documented in [rules-gala's `gazelle/README.md`](https://github.com/martianoff/rules-gala). If you follow only the main repo's dependency-management doc, which shows the plain Go `gazelle` rule, you'll hit `unknown directive: gazelle:gala_prefix` and produce no GALA targets. This is the single thing most likely to trip up a newcomer. (It's a doc gap, not a tooling gap — once you use the macro, it just works.)
- **Order of operations.** `gala mod tidy` only generates `go.mod` once `MODULE.bazel` is present — it detects "Bazel project" by that file. Run the Bazel scaffolding first, then `tidy`.
- **Cold build cost.** The first `bazel run //:gazelle` builds the GALA toolchain and the Go SDK from the registry — about a minute and a half on a cold cache here. Every build after that is single-digit seconds. This is ordinary Bazel behavior, not a GALA quirk.
- **Bazel-first.** This is the Bazel path. For a quick single binary you can skip all of it and use `gala build` / `gala run` directly; the Bazel story is for projects that want reproducible, registry-pinned, polyglot builds.

None of these are blockers. The worst case costs you one web search to find the right Gazelle macro. After that, adding another Go dependency is two commands — `gala mod add … --go` then `bazel run //:gazelle` — and the BUILD file updates itself.

---

## Why this is a bigger deal than a UUID

The reason "import a Go module" is the make-or-break test is that it's the entire value proposition of a transpile-to-Go language. If GALA's interop were shallow — stdlib only, or types collapsing to `any` at the boundary — then every time you reached for the Go ecosystem you'd fall out of the type system that made you choose GALA in the first place. You'd be writing Go with extra steps.

That's not what happened here. A third-party module dropped in, kept its types, and built through a production-grade Bazel toolchain with generated BUILD files. The recent work — third-party Go type inference, the extraction of `rules_gala` into its own registry-published module, and first-class Gazelle support — adds up to a build story where the polyglot seam is close to invisible. You get sealed types, exhaustive pattern matching, and `Option`/`Try`/`Either` on the GALA side, and the entire Go module ecosystem on the other, with concrete types across the boundary.

Try it before you commit to anything: the [GALA Playground](https://gala-playground.fly.dev) runs in the browser with no install. When you're ready for a real project, the steps above are the whole recipe — and `rules_gala` plus Gazelle make sure the build is the part you stop thinking about.
