Cross-compilation: binários para qualquer plataforma

1. Introdução à Cross-compilation em Go

Cross-compilation é a capacidade de gerar binários executáveis para um sistema operacional e arquitetura diferentes da máquina onde o código está sendo compilado. Em muitas linguagens, isso exige configurações complexas, toolchains específicas e bibliotecas compiladas para cada alvo. Em Go, essa tarefa é surpreendentemente simples e nativa.

O compilador Go é único — ele não depende de um runtime externo como a JVM ou o .NET Framework. Todo o runtime necessário (scheduler, garbage collector, gerenciamento de goroutines) é embutido no binário final. Isso significa que um binário Go compilado para Linux rodará em qualquer distribuição Linux sem precisar instalar dependências adicionais.

Os cenários de uso são vastos: desenvolvedores em macOS que precisam gerar binários para servidores Linux, equipes que trabalham com dispositivos IoT como Raspberry Pi (arquitetura ARM), ou a criação de imagens Docker minimalistas (scratch images). Com Go, você compila uma vez e executa em qualquer lugar — literalmente.

2. Configurando o Ambiente: Variáveis GOOS e GOARCH

O segredo da cross-compilation em Go está em duas variáveis de ambiente:

  • GOOS: define o sistema operacional alvo (ex: linux, darwin, windows, freebsd)
  • GOARCH: define a arquitetura de CPU alvo (ex: amd64, arm64, 386, arm)

Para descobrir todas as combinações suportadas pela sua versão do Go, execute:

go tool dist list

Isso exibirá uma lista completa como:

linux/amd64
linux/arm64
linux/arm
darwin/amd64
darwin/arm64
windows/amd64
windows/386
freebsd/amd64
openbsd/amd64
...

Cada linha representa uma combinação válida de sistema operacional e arquitetura que o compilador Go pode gerar.

3. Comandos Práticos para Compilação Cruzada

A sintaxe básica para cross-compilation é simples. Basta definir as variáveis antes do comando go build:

GOOS=linux GOARCH=amd64 go build -o meu-programa

Para Windows, lembre-se de adicionar a extensão .exe:

GOOS=windows GOARCH=amd64 go build -o app.exe

Compilando para Raspberry Pi (ARM 32 bits com suporte a hardware floating point):

GOOS=linux GOARCH=arm GOARM=7 go build -o app-arm

Para Apple Silicon (M1, M2):

GOOS=darwin GOARCH=arm64 go build -o app-macos-arm64

Você pode compilar para qualquer combinação sem precisar instalar nada adicional — o compilador Go já inclui tudo que precisa.

4. Lidando com Dependências e CGO

Aqui mora o principal desafio da cross-compilation em Go: o CGO. Quando seu código utiliza bibliotecas escritas em C (via import "C"), ou depende de pacotes que usam CGO (como mattn/go-sqlite3 ou gocv), a compilação cruzada se complica.

A solução mais comum é desabilitar o CGO:

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app

Isso gera um binário estático, sem dependências de bibliotecas C do sistema. O binário será maior, mas executará em qualquer distribuição Linux, mesmo em sistemas minimalistas como Alpine Linux (que usa musl em vez de glibc).

Se você precisa de CGO, terá que instalar toolchains específicas para cada plataforma alvo. Por exemplo:

# Para Windows a partir do Linux
GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc go build

# Para ARM Linux
GOOS=linux GOARCH=arm GOARM=7 CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc go build

Sempre que possível, prefira bibliotecas puras Go para evitar essa complexidade.

5. Otimizações e Boas Práticas

Binários Go podem ser grandes. Para reduzi-los significativamente:

go build -ldflags="-s -w" -trimpath -o app
  • -s -w: remove informações de debug e símbolos (redução de ~30% no tamanho)
  • -trimpath: remove caminhos absolutos do sistema de arquivos do desenvolvedor do binário

Para debugging (apenas em desenvolvimento), use flags que preservam informações:

go build -gcflags="all=-N -l" -o app-debug

Para automação, go generate permite embedar comandos de build no código fonte:

//go:generate GOOS=linux GOARCH=amd64 go build -o bin/linux/meuapp
//go:generate GOOS=windows GOARCH=amd64 go build -o bin/windows/meuapp.exe

Execute go generate ./... para compilar tudo de uma vez.

6. Automatizando Builds com Scripts e Makefiles

Um Makefile prático para compilar para as três plataformas principais:

APP_NAME = meuapp
VERSION = 1.0.0

.PHONY: build-all build-linux build-darwin build-windows clean

build-all: build-linux build-darwin build-windows

build-linux:
    GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \
        go build -ldflags="-s -w" -trimpath \
        -o dist/$(APP_NAME)-$(VERSION)-linux-amd64

build-darwin:
    GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 \
        go build -ldflags="-s -w" -trimpath \
        -o dist/$(APP_NAME)-$(VERSION)-darwin-amd64

build-windows:
    GOOS=windows GOARCH=amd64 CGO_ENABLED=0 \
        go build -ldflags="-s -w" -trimpath \
        -o dist/$(APP_NAME)-$(VERSION)-windows-amd64.exe

clean:
    rm -rf dist/

Para CI/CD com GitHub Actions, um workflow simples:

jobs:
  build:
    strategy:
      matrix:
        goos: [linux, darwin, windows]
        goarch: [amd64]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: '1.22'
      - run: |
          GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} \
          CGO_ENABLED=0 go build -ldflags="-s -w" -o app
      - uses: actions/upload-artifact@v4
        with:
          name: app-${{ matrix.goos }}-${{ matrix.goarch }}
          path: app

7. Testando e Validando Binários Compilados

Para verificar se o binário foi compilado corretamente, use o comando file (Linux/macOS):

file meuapp
# Saída: meuapp: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked

Para testar binários ARM sem ter um dispositivo físico, use Docker:

# Testar binário ARM64
docker run --platform linux/arm64 --rm -v $(pwd):/app alpine /app/meuapp-arm64

# Testar binário ARM 32 bits
docker run --platform linux/arm/v7 --rm -v $(pwd):/app alpine /app/meuapp-arm

Para inspeção mais detalhada, use as ferramentas do próprio Go:

go tool objdump -s main.main meuapp | head -20

8. Casos Avançados e Limitações

Go suporta plataformas menos comuns como FreeBSD, OpenBSD, Plan 9 e até Solaris:

GOOS=freebsd GOARCH=amd64 go build -o app-freebsd
GOOS=plan9 GOARCH=amd64 go build -o app-plan9

Limitações importantes:

  1. CGO: nem todas as combinações de GOOS/GOARCH suportam CGO. Verifique com go tool dist list -json
  2. Syscalls: algumas syscalls específicas do Linux (como epoll) não existem em outros sistemas. Use a biblioteca padrão que abstrai essas diferenças
  3. Build tags: para código específico de plataforma, use tags de compilação:
//go:build linux
package main

func init() {
    // Código específico para Linux
}
//go:build darwin
package main

func init() {
    // Código específico para macOS
}

A cross-compilation em Go é uma das features mais poderosas da linguagem. Com poucas linhas de comando, você pode gerar binários para praticamente qualquer plataforma existente, mantendo a simplicidade e eficiência que tornam Go uma escolha excepcional para desenvolvimento de sistemas e ferramentas de linha de comando.

Referências