CodeGym /Cursos /Go SELF /Cross-compilacao GOOS/GOARCH — conjunto minimo

Cross-compilacao GOOS/GOARCH — conjunto minimo

Go SELF
Nível 54, Lição 2
Disponível

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):

GOOS
GOARCH
O que isso geralmente significa Onde aparece
linux
amd64
servidor/container tipico backend, CI
linux
arm64
servidores ARM, Raspberry Pi, algumas nuvens IoT, ARM-cloud
darwin
arm64
macOS no Apple Silicon Macs modernos
windows
amd64
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.

1
Tarefa
Go SELF, nível 54, lição 2
Bloqueado
Passaporte da plataforma
Passaporte da plataforma
1
Tarefa
Go SELF, nível 54, lição 2
Bloqueado
Flag de diagnóstico
Flag de diagnóstico
1
Tarefa
Go SELF, nível 54, lição 2
Bloqueado
Pacote de metadados
Pacote de metadados
1
Tarefa
Go SELF, nível 54, lição 2
Bloqueado
Cross-compilação para dist
Cross-compilação para dist
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION