1. Por que a cross-compilacao e necessaria
Cross-compilacao parece “magoaria de nivel ‘compile Windows no Mac sem sair do Vim’”. Na pratica, e algo bem terreno: voce tem um projeto, mas os usuarios usam diferentes sistemas operacionais e CPUs. Queremos compilar um binario para um servidor Linux, para o notebook Windows de um colega e para um pequeno dispositivo ARM — sem reescrever o codigo. Em Go, boa parte disso e resolvida por duas variaveis de ambiente: GOOS e GOARCH.
Para nao se perder na terminologia, mantenha uma ideia simples na cabeca. O compilador sempre gera um binario “para algo especifico”, e esse “algo” chamamos de target (alvo). Ja o ambiente onde voce executa go build se chama host. Esses termos vao aparecer com frequencia, e daqui a pouco vamos fixa-los com exemplos.
Host vs target
Quando voce digita go build, essa acao acontece na sua maquina (ou no CI). Isso e o host: o sistema operacional e a arquitetura nos quais a ferramenta de build esta rodando. Mas o resultado do build — o binario — e destinado a executar em alguma plataforma. Esse e o target: o sistema operacional e a arquitetura para os quais o programa e compilado.
Uma analogia cotidiana importante: imagine que voce imprime uma instrucao em uma impressora. A impressora esta na sua casa (host), mas a instrucao e impressa para um amigo que tem um modelo de dispositivo diferente (target). O local da impressao nao muda o sentido do texto — voce apenas escolhe antecipadamente “para qual dispositivo” imprimir a instrucao. Em Go, voce “escolhe o dispositivo” por meio de GOOS/GOARCH.
Visualmente, isso pode ser representado assim:
flowchart LR
A[Host: onde executamos go build] -->|GOOS/GOARCH definem| B[Target: para o que compilamos o binario]
B --> C[Binario: executa apenas na plataforma do target]
E aqui vai uma consequencia pratica imediata: se voce compilou um binario para windows/amd64, ele nao vai rodar em Linux “simplesmente assim”. O binario vai tentar honestamente ser um programa do Windows, e o Linux vai honestamente dizer “eu nao sou Windows” (as vezes sem drama, de forma bem direta).
2. GOOS/GOARCH: escolha da plataforma de build
O que sao GOOS e GOARCH
GOOS e o sistema operacional alvo (operating system), e GOARCH e a arquitetura do processador alvo (architecture). Juntos, eles definem a plataforma do target. Por exemplo, windows/amd64 significa “Windows em arquitetura x86 de 64 bits”, e linux/arm64 significa “Linux em ARM64”.
Se voce ja viu esses parametros nao apenas em builds, mas tambem na documentacao do ecossistema Go, isso e normal: ate paginas da biblioteca padrao em sites de pkg podem levar GOOS/GOARCH em conta (por exemplo, para partes condicionais da API dependentes da plataforma). Nos dados de teste do site Go, aparecem explicitamente parametros como ?GOOS=windows&GOARCH=amd64.
O conjunto minimo de plataformas que costuma aparecer no desenvolvimento do dia a dia pode ser mantido em forma de tabela (sem tentar decorar todo o zoologico de uma vez — ele e grande):
|
|
O que isso geralmente significa | Onde aparece |
|---|---|---|---|
|
|
servidor/container tipico | backend, CI |
|
|
servidores ARM, Raspberry Pi, algumas nuvens | IoT, ARM-cloud |
|
|
macOS no Apple Silicon | Macs modernos |
|
|
Windows 64-bit | desktop |
As vezes voce pode encontrar combinacoes mais exoticas. Por exemplo, em noticias sobre Go, plataformas como WASI sao mencionadas (GOOS=wasip1, GOARCH=wasm). Mas, para o “conjunto minimo”, basta ficar confortavel com as quatro linhas da tabela acima.
Como definir GOOS/GOARCH no build
Agora e um bom momento para mostrar de forma simples e honesta “como isso fica na pratica”. GOOS e GOARCH sao variaveis de ambiente. Isso significa que voce pode defini-las “para um unico comando”, sem mudar nada globalmente (e esse e o modo mais seguro para iniciantes: menor chance de compilar tudo “para a plataforma errada”).
Em Linux/macOS, normalmente se escreve assim (na mesma linha antes do comando):
GOOS=windows GOARCH=amd64 go build -o todo.exe ./cmd/todo
No Windows (PowerShell), a sintaxe e diferente, mas a ideia e a mesma: voce define os valores no ambiente e executa go build. Aqui nao vamos aprofundar nas diferencas de shell — o importante e entender o principio: os valores de GOOS/GOARCH sao lidos do ambiente no momento em que go build e iniciado.
Por que -o faz sentido aqui? Porque estamos gerando um unico pacote main e esperamos um unico arquivo de saida. E tambem vemos um detalhe util: para Windows, normalmente e conveniente adicionar a extensao .exe ao nome do arquivo. Isso nao e uma exigencia do compilador “a qualquer custo”, e sim uma exigencia do ambiente/das expectativas do usuario: um usuario do Windows sem .exe costuma ficar confuso e suspeitar que foi enganado.
E sim, um fato cotidiano importante: se voce, no macOS, compilar todo.exe, isso nao significa que voce podera executa-lo imediatamente com duplo clique. Voce compilou “para Windows”, e o macOS nao e Windows. Isso nao e um erro do Go; e apenas a realidade.
Como descobrir o target de dentro do binario
Com cross-compilacao, ha um problema classico: voce gera rapidamente varios binarios e depois olha para a pasta pensando: “Hum... e esse aqui, e para o que? Ou sou genial, ou simplesmente esqueci de nomear os arquivos.” Para nao depender do nome do arquivo (que pode ser renomeado), e util fazer com que o proprio binario consiga dizer para qual plataforma foi compilado.
Em Go, isso e feito com o pacote runtime: nele ha valores parecidos com constantes runtime.GOOS e runtime.GOARCH. Um detalhe importante: isso nao significa “o sistema operacional no qual go build esta sendo executado agora”. Sao valores gravados no binario durante o build, ou seja, descrevem o target.
Mini-exemplo (voce pode executa-lo como um pequeno brinquedo para sentir a ideia):
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Printf("target: %s/%s\n", runtime.GOOS, runtime.GOARCH) // target: linux/amd64
}
Se voce compilar esse codigo para windows/amd64, ele vai imprimir target: windows/amd64 — mesmo que voce o tenha compilado em Linux/macOS. E exatamente isso que precisamos para diagnosticar artefatos.
4. Adicionando o target em --version
Agora vamos fazer um pequeno, mas muito pratico, upgrade no nosso aplicativo de estudo todo. Na licao anterior, ja tinhamos um pacote internal/buildinfo, onde ficam var Version, var Commit, var Date, e o main ja sabe imprimir essas informacoes com a flag --version e sair.
Agora vamos adicionar o target da plataforma. A ideia e fazer com que o comando:
todo --version
mostre nao apenas a versao e o commit, mas tambem, por exemplo, linux/amd64 ou windows/amd64. Assim, qualquer pessoa que receber o binario pode verificar “o que isso realmente e”, sem recorrer a rituais de adivinhacao.
Atualize internal/buildinfo: adicione Target()
Ao entrar nesta secao, e importante entender a ideia de design: as informacoes de build devem viver em um so lugar, para nao serem espalhadas pelo projeto. Por isso, tambem vamos adicionar o target ao buildinfo. E e la que vamos formatar a saida.
package buildinfo
import "runtime"
// Target returns the platform for which the binary was built.
func Target() string {
return runtime.GOOS + "/" + runtime.GOARCH
}
O codigo e curto, quase “simples demais”, mas e exatamente esse tipo de coisa que torna o produto conveniente: o usuario nao precisa adivinhar, voce mesmo informou tudo.
Vamos montar um formato unico de versao
Agora queremos juntar tudo em uma linha, para que main fique o mais burro possivel: “se pediram a versao, imprima o que ja esta pronto e saia”. Isso reduz a chance de voce imprimir a versao de um jeito em um lugar e de outro jeito em outro.
package buildinfo
import "fmt"
var Name = "todo"
var Version = "dev"
var Commit = "none"
func FullVersion() string {
return fmt.Sprintf("%s %s (%s) %s", Name, Version, Commit, Target())
}
Aqui usamos var porque queremos conseguir injetar Version e Commit via -ldflags -X (como vimos antes). Ja Target() vem de runtime e e “automaticamente correto” para o binario compilado.
Vamos conectar isso no main
Agora vem um ponto importante de UX de linha de comando: --version deve funcionar rapido e nao disparar a logica principal do aplicativo. Isso e especialmente importante quando a logica principal pode exigir configuracao, arquivos, rede — qualquer coisa. Versao e diagnostico, nao “execucao do processo de negocio”.
package main
import (
"flag"
"fmt"
"example.com/todo/internal/buildinfo"
)
func main() {
showVersion := flag.Bool("version", false, "print version and exit")
flag.Parse()
if *showVersion {
fmt.Println(buildinfo.FullVersion()) // todo dev (none) linux/amd64
return
}
fmt.Println("todo: app started")
}
Agora qualquer binario seu informa sozinho o que ele e. Isso e especialmente util quando voce tem dois arquivos em maos: um para Linux, outro para Windows, e nao quer adivinhar pelo nome.
5. Pratica: extensao .exe, execucao e expectativas
A cross-compilacao muitas vezes embaralha a cabeca do iniciante nao porque seja complicada, mas porque as expectativas nao batem com a realidade. Parece: “Acabei de compilar, por que nao executa?” — porque voce compilou “para outro planeta”. Por isso e util manter tres regras praticas logo de inicio.
Primeira regra: o nome do binario deve sugerir a platform. Mesmo que voce tenha adicionado o target em --version, ainda e mais conveniente para a pessoa ver pelo nome do arquivo o que foi baixado. Normalmente, o nome reflete pelo menos GOOS e GOARCH, e no Windows se adiciona .exe.
Segunda regra: se voce compilou para Windows, rode no Windows. Se compilou para Linux, rode no Linux. Isso parece obvio, mas na primeira vez o cerebro resiste e diz: “Mas o computador e o mesmo...”. Nao, o sistema operacional faz parte da plataforma.
Terceira regra: nao complique a cross-compilacao antes da hora. Sim, existem detalhes, como cgo e dependencias externas em C, mas, no “conjunto minimo”, assumimos que temos um aplicativo Go comum, usando a biblioteca padrao, e que a cross-compilacao para ele realmente se resume a duas variaveis de ambiente. Quando um dia voce encontrar nuances, ja vai entender o modelo basico e conseguira descobrir com precisao o que esta atrapalhando.
6. Erros tipicos na cross-compilacao GOOS/GOARCH
Erro #1: confundir host e target e esperar que o que foi compilado obrigatoriamente rode na maquina atual.
Um cenario muito comum: no macOS, voce definiu GOOS=windows, obteve um arquivo, tentou executa-lo — e nada. Isso nao e “bug do build”, e comportamento correto: voce criou um programa do Windows, mas esta tentando executa-lo no macOS. A cura e voltar ao modelo host/target: onde compilamos nao e igual a onde executamos.
Erro #2: trocar GOOS e GOARCH de lugar ou definir um par impossivel.
Quando a cabeca ainda esta confusa, e facil escrever algo como GOOS=amd64 GOARCH=windows. O Go normalmente vai dizer rapidamente que isso nao e valido, mas voce vai perder tempo tentando entender “por que ele esta reclamando”. Um habito util e manter uma tabelinha curta das combinacoes populares e nao tentar adivinhar.
Erro #3: esquecer a extensao .exe no Windows e depois se surpreender com “por que nao roda no computador do usuario”.
Tecnicamente, o binario pode existir sem extensao, mas o usuario do Windows espera .exe, e muitas ferramentas/exploradores dependem disso. Por isso, se o alvo e Windows, vale a pena dar desde o momento de -o um nome claro com .exe. Isso nao e um “capricho do Go”, e respeito pela plataforma.
Erro #4: nao saber verificar para o que o binario foi compilado e viver pelo principio “acho que e Linux”.
Quando os artefatos se multiplicam, adivinhar quebra imediatamente. A solucao e simples: adicione em --version a impressao de runtime.GOOS/runtime.GOARCH (por exemplo, por meio de buildinfo.Target()), e a verificacao vira um unico comando. Voce para de adivinhar — o proprio binario informa tudo.
Erro #5: compilar plataformas diferentes, mas imprimir a mesma “versao” sem target, e depois confundir relatorios de erro.
Se o usuario escreve: “Meu todo v1.2.3 cai”, e voce nao tem informacao da plataforma, vai acabar fazendo perguntas desnecessarias. E muito mais conveniente quando a versao fica assim: “todo 1.2.3 (abc123) windows/amd64”: isso economiza tempo para voce e para o usuario.
GO TO FULL VERSION