Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Go Workflows

This page covers Go-related workflows in [yoe]: building Go binaries as units, where the Go module/build cache lives, and how to recover from common pitfalls.

Building a Go binary as a unit

The go_binary class in module-core/classes/go.star wraps the standard build pattern: clone the source, run go build, install the resulting binary into $DESTDIR$PREFIX/bin. A minimal unit looks like:

load("//classes/go.star", "go_binary")

go_binary(
    name = "siot",
    version = "0.18.5",
    source = "https://github.com/simpleiot/simpleiot.git",
    tag = "v0.18.5",
    go_package = "./cmd/siot",   # optional, defaults to ./cmd/<name>
    binary = "siot",             # optional, defaults to <name>
    license = "Apache-2.0",
)

Cross-compilation is automatic: yoe sets GOARCH from the target machine’s arch (x86_64amd64, arm64arm64, riscv64riscv64) and forces CGO_ENABLED=0 GOOS=linux so the result is a statically-linked Linux binary.

The build runs inside the upstream golang:1.24 container by default. Override with container = "golang:1.23" (or any pullable Go image) when a unit needs a specific toolchain version.

Cache layout

Go-class units share a project-scoped cache mounted into the build container at /go/cache:

Inside containerOn hostPurpose
/go/cache/mod<project>/cache/go/modGOMODCACHE — downloaded modules (go.bug.st/serial@v1.6.4/...)
/go/cache/build<project>/cache/go/buildGOCACHE — compiled package artifacts

The cache survives across builds, so the second build of any Go unit is much faster than the first. It also survives across different Go units in the same project — simpleiot and a hypothetical second Go unit will share downloaded modules and built artifacts.

Cleaning the cache

go mod download writes module files with mode 0444 and their parent directories with mode 0555 — read-only by design, so you can’t accidentally edit a cached module. As a side effect, rm -rf against the cache fails with “Permission denied” even though every file is owned by your user: unlink needs the write bit on the parent directory, and Go has stripped it.

Recover with one of these:

# Option 1: chmod first, then rm — fastest
chmod -R u+w <project>/cache/go && rm -rf <project>/cache/go

# Option 2: let Go do it (this is what `go clean` is for)
GOMODCACHE=<project>/cache/go/mod go clean -modcache
GOCACHE=<project>/cache/go/build go clean -cache

Symptom you’d see if you skip the chmod:

rm: cannot remove 'yoe-test/cache/go/mod/go.bug.st/serial@v1.6.4/serial.go': Permission denied

This is a permission-bit issue, not an ownership issue. stat -c '%U:%G' on the file will show your username, not root.

Build sandbox notes

Go builds run inside the build container as --user $(id -u):$(id -g), so all cache writes land on the host as your user. The golang:1.24 upstream image has /go owned by root, but the bind mount overlays that with the project cache directory before any build step runs.

Cross-arch Go builds (e.g. building a riscv64 binary on x86_64) use QEMU user-mode emulation via binfmt_misc. Run yoe container binfmt once to register the QEMU handlers if you haven’t yet — the TUI surfaces a warning banner when this is missing.

Customising the build

Common knobs on go_binary:

FieldDefaultNotes
go_package./cmd/<name>Path passed as the final arg to go build.
binary<name>Installed filename when it differs from the unit name (e.g. simpleiotsiot).
containergolang:1.24Override to pin a different toolchain.
tasksemptyExtra tasks (e.g. installing init scripts) merged after the default build task.
runtime_depsemptyPackages installed alongside the binary on the device (e.g. openrc for the service).

For anything more involved than a go build — multi-binary repos, code generation, embedded assets — drop down to the plain unit() class and write the task("build", steps=[...]) directly. go_binary is just a thin wrapper around that pattern.