Bem-vindo à aula 82 do nosso curso de Go! Hoje vamos mergulhar no processo de compilação de programas Go, um tópico essencial para qualquer desenvolvedor que deseja distribuir seus aplicativos. Você aprenderá a usar o comando go build de forma eficiente, a configurar a compilação para diferentes sistemas operacionais e arquiteturas (cross-compilation), a utilizar flags de build para controlar o processo e a gerar binários estáticos, que são executáveis independentes que não dependem de bibliotecas externas.

Dominar esses conceitos é fundamental para criar ferramentas portáteis, otimizar o tamanho dos executáveis e garantir que seus programas rodem em qualquer ambiente, seja em um servidor Linux, um desktop Windows ou um dispositivo embarcado. Vamos começar!

go build

O comando go build é a ferramenta principal para compilar código Go. Ele compila os pacotes indicados e gera um executável binário. Se você rodar go build dentro de um diretório que contém um pacote principal (package main), ele criará um executável com o nome do diretório (no Linux/macOS) ou com o nome do pacote (no Windows, com extensão .exe). Por exemplo, se você tem um diretório chamado hello com um arquivo main.go, o comando go build gerará um executável chamado hello (ou hello.exe).

Vamos ver um exemplo simples. Crie um arquivo main.go com o seguinte conteúdo:

package main

import "fmt"

func main() {
    fmt.Println("Olá, mundo!")
}

No terminal, navegue até o diretório onde o arquivo está e execute go build. Depois, execute o binário gerado. Você verá a mensagem "Olá, mundo!" impressa. O comando go build também aceita um caminho específico, como go build ./cmd/app, para compilar um pacote específico. Além disso, é possível usar a flag -o para especificar o nome do arquivo de saída:

go build -o meuapp main.go

Isso gera um executável chamado meuapp (sem extensão, a menos que você adicione). Essa flexibilidade é útil para controlar o nome do binário.

GOOS e GOARCH

Uma das características mais poderosas de Go é a capacidade de compilar para diferentes sistemas operacionais e arquiteturas de CPU a partir de uma única máquina, sem precisar de máquinas virtuais ou contêineres. Isso é feito por meio das variáveis de ambiente GOOS (sistema operacional alvo) e GOARCH (arquitetura alvo).

Os valores mais comuns para GOOS incluem linux, windows, darwin (macOS), freebsd, android e ios. Para GOARCH, temos amd64 (processadores Intel/AMD de 64 bits), 386 (32 bits), arm (processadores ARM de 32 bits), arm64 (ARM de 64 bits, como M1/M2) e wasm (WebAssembly). Para compilar para um alvo específico, defina essas variáveis antes de executar go build. Por exemplo, para gerar um executável para Windows 64 bits a partir do Linux, você faria:

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

No Windows, a sintaxe é um pouco diferente, usando set:

set GOOS=linux
set GOARCH=amd64
go build -o app

É importante notar que a cross-compilation não é possível para todos os alvos; por exemplo, para compilar para macOS, você precisa estar em um macOS ou usar uma máquina virtual, devido a restrições de licenciamento da Apple. Mas para a maioria dos alvos, funciona perfeitamente.

Flags de build

O comando go build oferece várias flags que permitem controlar o processo de compilação. Vamos explorar as mais úteis:

  • -o: especifica o nome do arquivo de saída. Já vimos isso.
  • -v: exibe os nomes dos pacotes que estão sendo compilados, útil para depuração.
  • -x: mostra os comandos executados, útil para entender o que está acontecendo nos bastidores.
  • -ldflags: permite passar opções para o linker. Por exemplo, -ldflags "-s -w" reduz o tamanho do binário removendo símbolos de debug e informações de tabela de símbolos.
  • -trimpath: remove caminhos do sistema de arquivos do binário, melhorando a reprodutibilidade.
  • -race: habilita o detector de corrida de dados (data race detector), útil para testes, mas não para produção.
  • -tags: permite compilar com tags de build, que são condições para incluir ou excluir arquivos. Isso é útil para código específico de plataforma ou funcionalidades opcionais.

Um exemplo de uso de -ldflags para reduzir o tamanho do binário:

go build -ldflags "-s -w" -o meuapp main.go

Essa técnica é muito comum em projetos que precisam de binários pequenos, como ferramentas de linha de comando.

Binários estáticos

Binários estáticos são executáveis que não dependem de bibliotecas dinâmicas (DLLs no Windows, .so no Linux, .dylib no macOS) do sistema. Eles incluem todo o código necessário dentro do próprio arquivo. Go, por padrão, produz binários estáticos, exceto em alguns casos específicos, como quando você usa a biblioteca os/user ou net com cgo, que pode exigir bibliotecas dinâmicas. Para garantir um binário 100% estático, você pode desabilitar o cgo, que é a ponte para código C, usando a variável de ambiente CGO_ENABLED=0.

Vamos ver um exemplo. Se você tiver um programa que usa a biblioteca net, como um servidor HTTP, o binário gerado pode depender de bibliotecas do sistema. Para garantir que seja estático, compile assim:

CGO_ENABLED=0 go build -o meuapp main.go

Binários estáticos são ideais para distribuição, pois funcionam em qualquer sistema com a mesma arquitetura, sem se preocupar com dependências. Eles também são mais fáceis de usar em contêineres, como Docker, porque não precisam de bibliotecas base. No entanto, eles podem ser um pouco maiores, pois incluem tudo.

Para verificar se um binário é estático, você pode usar o comando file no Linux/macOS:

file meuapp

Se a saída indicar "statically linked", então está estático.

Boas práticas e observações finais

Ao trabalhar com builds, é importante ter um processo claro. Sempre use go mod para gerenciar dependências e mantenha o código organizado. Ao distribuir binários, considere criar scripts de build que definam as variáveis de ambiente e as flags apropriadas. Além disso, use a flag -trimpath para garantir que os binários sejam reprodutíveis, o que é essencial para releases. Lembre-se de que a cross-compilation é uma ferramenta poderosa, mas teste os binários na plataforma alvo sempre que possível, pois podem haver diferenças sutis de comportamento.

Referências

Exercícios

  1. Crie um programa Go que imprima "Olá, mundo!" e compile-o para Linux (amd64) e Windows (amd64) a partir do seu sistema operacional atual. Verifique os binários gerados.

    ✓ Resposta: Crie um arquivo main.go com o conteúdo:
    package main
    
    import "fmt"
    
    func main() {
        fmt.Println("Olá, mundo!")
    }
    
    Depois, execute no terminal:
    GOOS=linux GOARCH=amd64 go build -o hello-linux main.go
    GOOS=windows GOARCH=amd64 go build -o hello.exe main.go
    
    Isso gerará os binários hello-linux e hello.exe no diretório atual.
  2. Use a flag -ldflags para reduzir o tamanho de um binário do seu programa. Compare o tamanho antes e depois.

    ✓ Resposta: Compile normalmente com go build -o app main.go e depois com go build -ldflags "-s -w" -o app-stripped main.go. Use ls -lh para comparar os tamanhos. O binário app-stripped será significativamente menor.
  3. Escreva um programa que use a biblioteca net/http e compile-o com CGO_ENABLED=0. Verifique se o binário é estático usando o comando file (Linux/macOS).

    ✓ Resposta: Crie um arquivo server.go com um servidor HTTP simples:
    package main
    
    import (
        "fmt"
        "net/http"
    )
    
    func handler(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Olá, servidor!")
    }
    
    func main() {
        http.HandleFunc("/", handler)
        http.ListenAndServe(":8080", nil)
    }
    
    Compile com CGO_ENABLED=0 go build -o server server.go. Depois, execute file server e veja se a saída contém "statically linked".
  4. Explique a diferença entre binários estáticos e dinâmicos e por que Go tende a produzir binários estáticos por padrão.

    ✓ Resposta: Binários estáticos incluem todas as bibliotecas necessárias dentro do executável, enquanto binários dinâmicos dependem de bibliotecas compartilhadas do sistema. Go produz binários estáticos por padrão porque a linguagem foi projetada para gerar executáveis independentes, usando sua própria runtime e biblioteca padrão compilada diretamente no binário. Isso facilita a distribuição e evita problemas de compatibilidade de versões de bibliotecas.
  5. Pesquise sobre a variável GOOS e GOARCH e liste 5 combinações possíveis (além das mencionadas) que podem ser usadas para cross-compilation.

    ✓ Resposta: Exemplos de combinações: freebsd/amd64, linux/386, darwin/arm64, windows/arm64, linux/arm. Essas combinações permitem compilar para FreeBSD, Linux 32 bits, macOS ARM (M1), Windows ARM e Linux ARM, respectivamente. Consulte a lista completa em https://go.dev/doc/install/source#environment.