← Painel 60 min
Primeiros passos

Hello, World e a toolchain: go run, go build e go mod sem mistério

Pré-requisitos: 01-bem-vindo-ao-go

Intuição

Pense na diferença entre cozinhar na sua casa e entregar comida congelada pronta. Quando você cozinha em casa, precisa da cozinha inteira: fogão, panelas, temperos, o fogo ligado durante todo o preparo. Quando entrega o prato congelado, a pessoa que recebe só precisa de um micro-ondas — todo o trabalho já foi feito antes, de uma vez, na sua cozinha.

Linguagens interpretadas como Python e JavaScript funcionam no primeiro modelo: para o seu programa rodar na máquina de outra pessoa, essa máquina precisa da “cozinha” — o interpretador, na versão certa, com as dependências instaladas. Quem já perdeu uma tarde com conflito de versão de Node ou virtualenv quebrado conhece o custo. Go funciona no segundo modelo: o comando go build faz todo o trabalho de tradução uma vez, na sua máquina, e o resultado é um arquivo binário único que roda em qualquer máquina do mesmo sistema — sem Go instalado, sem runtime, sem node_modules, sem nada além do próprio arquivo.

Essa é a razão de Docker e Kubernetes serem distribuídos como binários que você baixa e executa. E é a habilidade central desta aula: entender a toolchain — o conjunto de ferramentas que acompanha a linguagem — e o papel de cada comando. São três que você vai usar todos os dias: go run (compila e executa de uma vez, para desenvolvimento), go build (compila e guarda o binário, para distribuição) e go mod (declara a identidade e as dependências do projeto). Mais o gofmt da aula passada, que formata; e o go vet, que aponta código suspeito antes de virar bug.

A boa notícia para quem vem de ecossistemas com fadiga de ferramentas: tudo isso vem junto, em um único instalador oficial. Não existe decisão a tomar entre bundlers, transpilers e gerenciadores concorrentes. go é um comando só, com subcomandos — e nas próximas seções você vê exatamente o que cada um faz com o seu código.

Uma nota honesta sobre esta aula dentro do curso: aqui no navegador, o editor esconde a toolchain de você — os botões Executar e gofmt chamam o compilador e o formatador reais por baixo. Mas você está aprendendo Go para trabalhar com Go, e no trabalho a interface é o terminal. Esta aula é a ponte: quando você instalar Go na sua máquina (instrução de uma linha em go.dev/dl), cada comando daqui funciona idêntico.

Código anotado

Vamos escrever o programa que acompanha essa conversa: um “Hello, World” que se apresenta e mostra em qual sistema e arquitetura foi compilado — a mesma informação que os comandos da toolchain usam para gerar binários para cada plataforma.

O início você já conhece da aula passada: pacote executável e imports. A novidade é importar dois pacotes: a lista entre parênteses é a forma idiomática quando há mais de um. runtime expõe informações sobre o ambiente em que o programa está rodando.

package main

import (
	"fmt"
	"runtime"
)

runtime.GOOS e runtime.GOARCH são constantes gravadas no momento da compilação: o sistema operacional alvo (linux, darwin, windows, js…) e a arquitetura de processador (amd64, arm64, wasm…). Elas não descobrem nada em tempo de execução — o compilador as fixa ao gerar o binário. Um binário compilado para Linux dirá linux para sempre, em qualquer lugar.

// platform descreve o alvo de compilação deste binário.
func platform() string {
	return runtime.GOOS + "/" + runtime.GOARCH
}

O main imprime a apresentação. Rode e repare na saída: aqui no curso ela será js/wasm, porque o compilador que roda no seu navegador gera WebAssembly. O mesmo programa, compilado no seu notebook, diria darwin/arm64 ou linux/amd64. Um código, muitos alvos — esse é o mecanismo por trás da cross-compilation que você vai exercitar adiante.

func main() {
	fmt.Println("Hello, World!")
	fmt.Println("Compilado para:", platform())
}

Agora, o caminho que esse código percorre fora do navegador. Suponha o arquivo salvo como hello.go em um diretório vazio. O fluxo de trabalho real tem três passos, e cada um responde uma pergunta diferente.

Passo 1 — dar identidade ao projeto. Todo projeto Go moderno é um módulo: um diretório com um arquivo go.mod na raiz. O comando go mod init github.com/maria/hello cria esse arquivo com duas linhas: o caminho do módulo e a versão de Go. O caminho segue a convenção host/usuário/repositório porque ele é, ao mesmo tempo, o endereço de importação: se outro projeto quiser usar seu código, importa por esse nome. Para experimentos locais um nome curto funciona, mas o hábito da convenção não custa nada e evita renomear depois.

Passo 2 — rodar durante o desenvolvimento. go run hello.go compila o programa para um local temporário, executa e descarta o binário. É o ciclo rápido: editar, go run, olhar a saída, repetir. Nada fica para trás no diretório.

Passo 3 — construir para entregar. go build compila e grava o binário no diretório atual — hello (ou hello.exe no Windows). Esse arquivo é autônomo: copie-o para outra máquina do mesmo sistema/arquitetura e ele roda, sem Go instalado. A diferença entre go run e go build é só o destino do binário; a compilação é a mesma.

E o truque que fecha o raciocínio: as constantes GOOS e GOARCH que o programa imprime são configuráveis na compilação. GOOS=linux GOARCH=amd64 go build — rodado do seu Mac — produz um binário de Linux, pronto para o servidor. Nenhuma máquina virtual, nenhum toolchain extra: cross-compilation é parte do compilador padrão.

Experimente

Antes de rodar, preveja o que este programa imprime na segunda linha, sabendo que o compilador do curso gera WebAssembly para o navegador. Depois rode e confira.

package main

import (
	"fmt"
	"runtime"
)

func main() {
	fmt.Println("Hello, World!")
	fmt.Println("Compilado para:", runtime.GOOS+"/"+runtime.GOARCH)
	fmt.Println("Versão da linguagem:", runtime.Version())
}

Agora experimente: desalinhe a indentação de uma linha, adicione espaços a mais em volta do + e clique em gofmt — repare que ele arruma tudo sem perguntar nada. Depois remova o import de runtime deixando as chamadas, execute e leia o erro: o compilador aponta cada uso sem import. Por fim, deixe o import e remova os usos — o erro inverte para imported and not used. Esses dois erros vão aparecer no seu dia a dia; conhecê-los agora economiza minutos depois.

Exercício 1

Monte o caminho do módulo

Todo go mod init recebe um caminho de módulo, e a convenção do ecossistema é github.com/<usuário>/<repositório>. Implemente ModulePath(user string, repo string) string que monta esse caminho.

Entrada: o nome de usuário em user e o nome do repositório em repo, ambos não vazios e sem barras. Saída: a string "github.com/<user>/<repo>".

Exemplos:

  • ModulePath("maria", "todo-cli")"github.com/maria/todo-cli";
  • ModulePath("golang-br", "curso")"github.com/golang-br/curso".
Solução & Explicação

Três partes fixas, duas barras, concatenação direta:

func ModulePath(user string, repo string) string {
	return "github.com/" + user + "/" + repo
}

Custo O(n) no tamanho do resultado, sem casos de borda porque o contrato garante entradas limpas. A alternativa fmt.Sprintf("github.com/%s/%s", user, repo) produz a mesma string; para duas substituições em um template curto, o + deixa cada pedaço literal visível e dispensa a interpretação da string de formato. A armadilha é puramente de atenção: esquecer a barra do meio produz github.com/mariatodo-cli, que parece certo de relance — e o painel de testes mostra a diferença na comparação esperado × recebido. Vale registrar por que esse formato importa: o caminho do módulo é o nome que outros projetos usarão em import, então errar aqui significa quebrar todo mundo que depende de você.

Exercício 2

Gere o comando de cross-compilation

Sua equipe compila o mesmo serviço para vários alvos, e você quer gerar os comandos a partir de uma configuração. Implemente BuildCommand(goos string, goarch string, output string) string que monta a linha de comando de cross-compilation.

Entrada: o sistema alvo em goos, a arquitetura em goarch (ambos não vazios) e o nome do binário em output, que pode ser vazio. Saída: a string "GOOS=<goos> GOARCH=<goarch> go build -o <output>" — ou, quando output é vazio, apenas "GOOS=<goos> GOARCH=<goarch> go build", sem -o e sem espaço sobrando no final.

Exemplos:

  • BuildCommand("linux", "amd64", "servidor")"GOOS=linux GOARCH=amd64 go build -o servidor";
  • BuildCommand("windows", "arm64", "")"GOOS=windows GOARCH=arm64 go build".

A novidade em relação ao exercício 1 é a decisão: parte do comando só existe em um dos casos. Cuidado com o espaço antes de -o — ele pertence ao sufixo, não ao prefixo.

Solução & Explicação

Monte o prefixo comum uma vez; decida o sufixo depois:

func BuildCommand(goos string, goarch string, output string) string {
	command := fmt.Sprintf("GOOS=%s GOARCH=%s go build", goos, goarch)
	if output == "" {
		return command
	}
	return command + " -o " + output
}

O retorno antecipado no caso vazio é o padrão que você verá em todo código Go: trate o caso simples e saia, deixando o caminho principal sem indentação extra. Custo O(n) no tamanho do comando.

A alternativa que parece equivalente é escrever dois Sprintf completos, um em cada ramo do if. Funciona hoje — e é exatamente por isso que é armadilha: as duas strings quase iguais são duplicação, e a primeira mudança futura (digamos, adicionar -trimpath) será feita em um ramo e esquecida no outro. Testes só pegam isso se alguém lembrar de testar os dois ramos de novo. Outra falha comum é colar o espaço no lugar errado: command + "-o " produz build-o servidor. Quando uma string é montada por partes, cada separador precisa de dono definido — aqui, o espaço pertence ao sufixo " -o ".

Exercício 3

Reproduza a saída de go version

go version é o primeiro comando que você roda para diagnosticar qualquer ambiente Go — e a primeira coisa que vão te pedir em qualquer issue de bug. Implemente VersionLine(major int, minor int, patch int, goos string, goarch string) string que reproduz exatamente essa saída.

Entrada: os três componentes numéricos da versão e o par sistema/arquitetura. Saída: a string "go version go<major>.<minor>.<patch> <goos>/<goarch>".

Exemplos:

  • VersionLine(1, 26, 1, "darwin", "arm64")"go version go1.26.1 darwin/arm64";
  • VersionLine(1, 26, 1, "js", "wasm")"go version go1.26.1 js/wasm" — o ambiente exato onde seu código roda neste curso.

Este exercício combina as duas aulas: números dentro de string exigem a técnica do exercício 2 da aula 1. Repare no go minúsculo colado na versão — go1.26.1, sem espaço.

Solução & Explicação

Cinco valores, um template, uma linha:

func VersionLine(major int, minor int, patch int, goos string, goarch string) string {
	return fmt.Sprintf("go version go%d.%d.%d %s/%s", major, minor, patch, goos, goarch)
}

A força do Sprintf aparece exatamente neste formato denso: o template "go version go%d.%d.%d %s/%s" mostra a frase inteira com pontos, espaços e barra nas posições exatas. Custo O(n) no tamanho da linha.

A alternativa por concatenação exige strconv.Itoa para cada inteiro — lembre da aula 1: Go não converte int em string implicitamente — e o resultado são sete pedaços costurados com seis +, onde um ponto trocado por espaço passa despercebido no olho e reprova no teste. Quando o formato tem mais de dois ou três buracos, o template ganha. A pegadinha específica deste formato é o go duplicado: a saída real tem go version e o prefixo go colado na versão. Quem “corrige” o que parece redundância devolve go version 1.26.1 ... e falha os três casos — reproduzir uma saída existente significa reproduzi-la, não melhorá-la.

Resumo

  • go run compila, executa e descarta — o ciclo de desenvolvimento. go build compila e grava um binário autônomo que roda sem Go instalado — o ciclo de entrega.
  • go mod init <caminho> cria o go.mod, que dá identidade ao projeto; a convenção github.com/usuário/repo faz o caminho do módulo ser também o endereço de importação.
  • runtime.GOOS e runtime.GOARCH são fixadas na compilação, e as variáveis GOOS/GOARCH no go build fazem cross-compilation nativa — um Mac gera binário de Linux sem ferramenta extra.
  • Import não usado e identificador sem import são erros de compilação — você viu os dois no Experimente e vai revê-los a vida inteira.
  • A toolchain inteira vem no instalador oficial e dispensa configuração: essa uniformidade é parte do design da linguagem, não acidente.

Na próxima aula, imersao/03-valores-e-tipos, você sai das ferramentas e entra na linguagem: os tipos básicos de Go, o que a tipagem estática garante e por que int e string não se misturam sem permissão explícita — algo que você já sentiu nos exercícios de hoje.

Essa lição ficou clara?