Golang

Artigo 49 — Compilação cruzada, binários estáticos e Docker com Go Já leu

10 min de leitura

Artigo 49 — Compilação cruzada, binários estáticos e Docker com Go
Artigo 49 — Compilação cruzada, binários estáticos e Docker com Go O binário como unidade de deploy Uma das vantagens mais práticas do Go é sua capacidade de

Artigo 49 — Compilação cruzada, binários estáticos e Docker com Go

Curso: Dominando Go em 1 Ano Prof. Ricardo Matos Módulo 9 — Deploy, Cloud e Carreira


O binário como unidade de deploy

Uma das vantagens mais práticas do Go é sua capacidade de produzir binários completamente autocontidos. Um binário Go não precisa de runtime instalado, não depende de bibliotecas compartilhadas do sistema e pode ser compilado para qualquer plataforma a partir de qualquer outra. Essa característica transforma o deploy em algo fundamentalmente simples: copie o arquivo, execute.

Docker amplifica essa vantagem. Com builds multistage, a imagem final contém apenas o binário e suas dependências mínimas de sistema — tipicamente menos de 20 MB, contra centenas de MB de uma imagem com runtime Java ou Python.


Compilação básica

# Compilar para a plataforma atual
go build -o bin/app ./cmd/api

# Com otimizações para produção
go build -ldflags="-w -s" -o bin/app ./cmd/api

As flags -w e -s removem informações de debug e a tabela de símbolos, reduzindo o tamanho do binário em 30-40% sem impacto na funcionalidade.


Embutindo metadados via ldflags

// cmd/api/main.go
package main

import (
    "fmt"
    "os"
)

// Variáveis preenchidas em tempo de build via -ldflags
var (
    Versao    = "desenvolvimento"
    Commit    = "desconhecido"
    BuildTime = "desconhecido"
    GoVersion = "desconhecido"
)

func exibirVersao() {
    fmt.Printf("Versão:    %s\n", Versao)
    fmt.Printf("Commit:    %s\n", Commit)
    fmt.Printf("Build:     %s\n", BuildTime)
    fmt.Printf("Go:        %s\n", GoVersion)
}

func main() {
    if len(os.Args) > 1 && os.Args[1] == "version" {
        exibirVersao()
        return
    }
    // ... resto da aplicação
}
# Build com metadados
go build \
    -ldflags="-w -s \
        -X main.Versao=1.2.3 \
        -X main.Commit=$(git rev-parse --short HEAD) \
        -X main.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
        -X main.GoVersion=$(go version | awk '{print $3}')" \
    -o bin/app ./cmd/api

# Verificando
./bin/app version
# Versão:    1.2.3
# Commit:    a1b2c3d
# Build:     2024-03-15T10:30:00Z
# Go:        go1.22.0

Compilação cruzada

Go suporta compilação cruzada nativamente — sem ferramentas externas, sem containers, sem configurações complexas. As variáveis GOOS e GOARCH controlam o alvo:

# Linux AMD64 (servidores na nuvem)
GOOS=linux GOARCH=amd64 go build -o bin/app-linux-amd64 ./cmd/api

# Linux ARM64 (AWS Graviton, Raspberry Pi 4)
GOOS=linux GOARCH=arm64 go build -o bin/app-linux-arm64 ./cmd/api

# macOS Intel
GOOS=darwin GOARCH=amd64 go build -o bin/app-darwin-amd64 ./cmd/api

# macOS Apple Silicon
GOOS=darwin GOARCH=arm64 go build -o bin/app-darwin-arm64 ./cmd/api

# Windows 64-bit
GOOS=windows GOARCH=amd64 go build -o bin/app-windows-amd64.exe ./cmd/api

# Windows ARM64
GOOS=windows GOARCH=arm64 go build -o bin/app-windows-arm64.exe ./cmd/api

Plataformas suportadas:

# Lista todas as combinações GOOS/GOARCH suportadas
go tool dist list

Script de release multi-plataforma

#!/bin/bash
# scripts/release.sh

APP="taskapi"
VERSAO="${1:-dev}"
COMMIT=$(git rev-parse --short HEAD)
BUILD=$(date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS="-w -s -X main.Versao=${VERSAO} -X main.Commit=${COMMIT} -X main.BuildTime=${BUILD}"

PLATAFORMAS=(
    "linux/amd64"
    "linux/arm64"
    "darwin/amd64"
    "darwin/arm64"
    "windows/amd64"
)

mkdir -p dist

for PLATAFORMA in "${PLATAFORMAS[@]}"; do
    OS="${PLATAFORMA%/*}"
    ARCH="${PLATAFORMA#*/}"
    NOME="${APP}-${VERSAO}-${OS}-${ARCH}"
    EXT=""

    if [ "$OS" = "windows" ]; then
        EXT=".exe"
    fi

    echo "Compilando ${NOME}${EXT}..."

    GOOS="$OS" GOARCH="$ARCH" go build \
        -ldflags="$LDFLAGS" \
        -trimpath \
        -o "dist/${NOME}${EXT}" \
        ./cmd/api

    # Comprime com tar/zip
    if [ "$OS" = "windows" ]; then
        zip -j "dist/${NOME}.zip" "dist/${NOME}${EXT}"
    else
        tar -czf "dist/${NOME}.tar.gz" -C dist "${NOME}"
    fi

    # Remove binário não comprimido
    rm "dist/${NOME}${EXT}"
done

echo "Binários gerados em dist/"
ls -lh dist/

Binários estáticos: CGo e suas implicações

Por padrão, Go produz binários que dependem da libc do sistema para algumas operações — DNS e usuários do sistema em particular. Para binários verdadeiramente estáticos:

# Desabilita CGo completamente
CGO_ENABLED=0 go build -o bin/app ./cmd/api

# Verifica se o binário tem dependências dinâmicas
ldd bin/app
# ldd: bin/app: não é um executável dinâmico  ← binário estático

Com CGO_ENABLED=0, o Go usa implementações puras em Go para DNS e outras operações. Isso é essencial para:

  • Imagens Docker baseadas em scratch ou alpine
  • Deploy em ambientes sem libc compatível
  • Garantia de portabilidade máxima

Atenção com drivers de banco de dados CGo. O driver SQLite mattn/go-sqlite3 usa CGo. Com CGO_ENABLED=0, ele não compila. Use modernc.org/sqlite (puro Go) como alternativa.


Dockerfile multistage: do básico ao otimizado

Versão básica

FROM golang:1.22-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
    -ldflags="-w -s" \
    -o taskapi ./cmd/api

FROM alpine:3.19
COPY --from=builder /app/taskapi /usr/local/bin/taskapi
EXPOSE 8080
ENTRYPOINT ["taskapi"]

Versão otimizada com cache de layers

# syntax=docker/dockerfile:1
FROM golang:1.22-alpine AS deps

WORKDIR /app

# Cache separado para dependências — só rebuilda se go.mod/go.sum mudarem
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
    go mod download

FROM deps AS builder

ARG VERSAO=dev
ARG COMMIT=desconhecido
ARG BUILD_TIME=desconhecido

COPY . .

RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build \
        -ldflags="-w -s \
            -X main.Versao=${VERSAO} \
            -X main.Commit=${COMMIT} \
            -X main.BuildTime=${BUILD_TIME}" \
        -trimpath \
        -o /app/taskapi \
        ./cmd/api

# Imagem de produção
FROM gcr.io/distroless/static-debian12 AS producao

# Metadados da imagem
LABEL org.opencontainers.image.title="Task API"
LABEL org.opencontainers.image.version="${VERSAO}"
LABEL org.opencontainers.image.source="https://github.com/usuario/taskapi"

# Certificados TLS já incluídos na imagem distroless
COPY --from=builder /app/taskapi /taskapi

# Não executar como root
USER nonroot:nonroot

EXPOSE 8080
ENTRYPOINT ["/taskapi"]

A imagem distroless/static contém apenas: - Certificados CA para HTTPS - Arquivos de timezone - Usuário nonroot

Sem shell, sem gerenciador de pacotes, sem utilidades Unix. Menor superfície de ataque possível.


Comparativo de tamanhos de imagem

# Construindo variantes
docker build --target producao -t taskapi:distroless .
docker build -f Dockerfile.alpine -t taskapi:alpine .

# Comparando tamanhos
docker images taskapi
# REPOSITORY   TAG          SIZE
# taskapi      distroless   12.8MB
# taskapi      alpine       18.2MB
# taskapi      ubuntu       82.5MB  ← sem multistage

Build com GitHub Actions

# .github/workflows/release.yml
name: Release

on:
  push:
    tags:
      - 'v*'

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-binarios:
    name: Build Binários
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-go@v5
        with:
          go-version: '1.22'

      - name: Build multi-plataforma
        run: |
          make release VERSAO=${{ github.ref_name }}

      - name: Upload artefatos
        uses: actions/upload-artifact@v4
        with:
          name: binarios
          path: dist/

  build-docker:
    name: Build e Push Docker
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Login no GHCR
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Metadados da imagem
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=tag
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha

      - name: Build e push multi-arch
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          build-args: |
            VERSAO=${{ github.ref_name }}
            COMMIT=${{ github.sha }}
            BUILD_TIME=${{ github.event.head_commit.timestamp }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Build multi-arquitetura com buildx

# Criar builder com suporte multi-arch
docker buildx create --name multiarch --use
docker buildx inspect --bootstrap

# Build para múltiplas arquiteturas simultaneamente
docker buildx build \
    --platform linux/amd64,linux/arm64 \
    --build-arg VERSAO=1.2.3 \
    -t ghcr.io/usuario/taskapi:1.2.3 \
    -t ghcr.io/usuario/taskapi:latest \
    --push \
    .

# Verificar manifests
docker buildx imagetools inspect ghcr.io/usuario/taskapi:1.2.3

Embed: arquivos estáticos no binário

O pacote embed permite incluir arquivos estáticos diretamente no binário:

package main

import (
    "embed"
    "io/fs"
    "net/http"
)

//go:embed migrations/*.sql
var migrationFiles embed.FS

//go:embed web/dist
var staticFiles embed.FS

//go:embed config/defaults.yaml
var configPadrao []byte

func main() {
    // Acessa as migrations embutidas
    entries, _ := migrationFiles.ReadDir("migrations")
    for _, e := range entries {
        conteudo, _ := migrationFiles.ReadFile("migrations/" + e.Name())
        fmt.Println(e.Name(), len(conteudo), "bytes")
    }

    // Serve arquivos estáticos do frontend
    webFS, _ := fs.Sub(staticFiles, "web/dist")
    http.Handle("/", http.FileServer(http.FS(webFS)))

    // Usa configuração padrão embutida
    fmt.Println("Config padrão:", string(configPadrao))
}

Com embed, o deploy é um único arquivo — sem diretórios extras, sem preocupação com caminhos relativos.


Otimizações adicionais de build

# -trimpath remove caminhos do sistema de build do binário
# (importante para builds reproduzíveis e privacidade)
go build -trimpath ./cmd/api

# Gzip no binário para distribuição
gzip -9 -k bin/taskapi
ls -lh bin/
# bin/taskapi      12.8 MB
# bin/taskapi.gz    4.2 MB

# UPX para compressão com decompressão automática
# (cuidado: pode ser detectado como suspeito por antivírus)
upx --best bin/taskapi
ls -lh bin/
# bin/taskapi    4.8 MB

# Build com PGO (Profile-Guided Optimization) — Go 1.21+
# Coleta perfil de CPU de produção, usa para otimizar o próximo build
go build -pgo=cpu.pprof -o bin/taskapi-pgo ./cmd/api

Makefile completo de build

APP     := taskapi
VERSAO  := $(shell git describe --tags --always --dirty)
COMMIT  := $(shell git rev-parse --short HEAD)
BUILD   := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS := -ldflags "-w -s -X main.Versao=$(VERSAO) -X main.Commit=$(COMMIT) -X main.BuildTime=$(BUILD)"

.PHONY: build build-linux build-linux-arm build-all docker release clean

build:
    CGO_ENABLED=0 go build $(LDFLAGS) -trimpath -o bin/$(APP) ./cmd/api

build-linux:
    CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -trimpath -o bin/$(APP)-linux-amd64 ./cmd/api

build-linux-arm:
    CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build $(LDFLAGS) -trimpath -o bin/$(APP)-linux-arm64 ./cmd/api

build-all: build-linux build-linux-arm
    CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -trimpath -o bin/$(APP)-darwin-amd64 ./cmd/api
    CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -trimpath -o bin/$(APP)-darwin-arm64 ./cmd/api
    CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -trimpath -o bin/$(APP)-windows-amd64.exe ./cmd/api

docker:
    docker build \
        --build-arg VERSAO=$(VERSAO) \
        --build-arg COMMIT=$(COMMIT) \
        --build-arg BUILD_TIME=$(BUILD) \
        -t $(APP):$(VERSAO) \
        -t $(APP):latest \
        .

release: build-all docker
    @echo "Release $(VERSAO) pronto"

clean:
    rm -rf bin/ dist/

Resumo do que foi coberto

Este artigo apresentou compilação e Docker em Go de forma completa: flags de build para otimização e metadados embutidos via ldflags, compilação cruzada para todas as plataformas com GOOS e GOARCH, script de release multi-plataforma, binários estáticos com CGO_ENABLED=0, Dockerfile multistage com cache de layers e imagem distroless, comparativo de tamanhos de imagem, pipeline de CI com GitHub Actions para build multi-arch, embed para incluir arquivos estáticos no binário e Makefile completo de build. O próximo artigo explora deploy na nuvem com Railway, Fly.io e Google Cloud Run.


Referências e leituras complementares

  • Documentação go build — Referência de todas as flags de compilação. https://pkg.go.dev/cmd/go#hdr-Compile_packages_and_dependencies

  • distroless images — Imagens minimalistas do Google para produção. https://github.com/GoogleContainerTools/distroless

  • Docker multistage builds — Guia oficial de builds multistage. https://docs.docker.com/build/building/multi-stage/

  • Go embed — Documentação do pacote embed. https://pkg.go.dev/embed

  • Profile-Guided Optimization — PGO em Go 1.21+. https://go.dev/doc/pgo

  • Docker buildx — Build multi-plataforma com buildx. https://docs.docker.com/buildx/working-with-buildx/


Próximo artigo: Artigo 50 — Go na nuvem: deploy no Railway, Fly.io e Google Cloud Run**


you asked

Sim


claude response

Comentários

Mais em Golang

Variáveis, tipos primitivos e declaração curta
Variáveis, tipos primitivos e declaração curta

  O sistema de tipos do Go é sua primeira linha de defesa! ...

Instalação, configuração do ambiente e o comando `go`
Instalação, configuração do ambiente e o comando `go`

  Antes de escrever código, o ambiente que precisaremos deve est...

Operadores, expressões e conversão de tipo
Operadores, expressões e conversão de tipo

Se variáveis são os substantivos de um programa, operadores s&a...