Resposta rápida: o Air observa seu projeto Go, recompila o código e reinicia a aplicação automaticamente a cada alteração válida. Para uma API local, o começo mais simples é instalar github.com/air-verse/air, executar air init para gerar o arquivo .air.toml e trocar o comando manual go run ./cmd/api por air. Em equipes, prefira registrar a ferramenta no próprio módulo com go get -tool, configurar explicitamente build.cmd e build.entrypoint e manter a pasta de binários temporários fora do Git.
Go compila rápido, mas o ciclo “salvar, encerrar o processo, executar de novo e voltar ao navegador” fica cansativo durante o desenvolvimento de APIs. Air automatiza justamente essa parte. Ele não muda a forma como seu programa funciona e não adiciona dependência ao binário final: é uma ferramenta externa usada apenas no ambiente local.
Neste guia, você vai configurar Air em uma API Go, entender o .air.toml, integrar geração de código com Templ, rodar o fluxo em Docker Compose e diagnosticar os problemas mais comuns.
O que Air faz — e o que ele não faz
O fluxo básico do Air é:
- observar arquivos e diretórios definidos na configuração;
- esperar um pequeno intervalo para agrupar alterações consecutivas;
- encerrar o processo anterior;
- executar o comando de build;
- iniciar o novo binário se a compilação terminar sem erro;
- continuar observando o projeto.
Apesar de “hot reload” ser o termo mais buscado, Air normalmente faz um rebuild seguido de restart. Ele não injeta uma função nova dentro do processo em execução nem preserva memória, conexões ou goroutines do binário anterior.
Essa diferença importa. Se sua API mantém estado apenas em memória, esse estado desaparece a cada alteração. Banco de dados, Redis, arquivos e outros serviços externos continuam disponíveis, mas conexões são fechadas e abertas de novo. Seu programa deve inicializar e encerrar de forma previsível, inclusive com graceful shutdown em Go.
Air também não substitui:
go testpara validar comportamento;go vet, Staticcheck ou golangci-lint para análise;- Delve para debugging interativo;
- Docker Compose para subir dependências;
- um supervisor ou orquestrador em produção.
Pense nele como uma automação do loop local, não como parte da arquitetura da aplicação.
Air, go run, Makefile ou Docker Compose?
Cada opção resolve uma camada diferente:
| Ferramenta | Responsabilidade principal | Quando usar |
|---|---|---|
go run ./cmd/api | Compilar e executar uma vez | Teste rápido ou projeto pequeno |
| Makefile | Dar nomes estáveis a comandos | Padronizar build, teste e geração |
| Air | Observar, recompilar e reiniciar | Desenvolvimento iterativo |
| Docker Compose | Coordenar API e dependências | PostgreSQL, Redis, filas e ambiente reproduzível |
| Delve | Pausar e inspecionar execução | Investigar estado e fluxo do programa |
Você pode combinar as ferramentas. Um Makefile pode oferecer make dev, que executa Air; o Docker Compose pode iniciar um container que roda o mesmo comando; e Delve pode aparecer como build.entrypoint em uma configuração separada.
Se você ainda está montando seu primeiro serviço, comece pelo tutorial de API REST em Go e volte a este artigo quando já conseguir executar a aplicação manualmente.
Como instalar Air
O repositório atual é github.com/air-verse/air. Tutoriais antigos podem mostrar o namespace cosmtrek/air; prefira o caminho atual para instalar a ferramenta.
Instalação global
Com Go 1.25 ou mais recente:
go install github.com/air-verse/air@latest
Garanta que o diretório de binários do Go está no PATH:
export PATH="$PATH:$(go env GOPATH)/bin"
air -v
A instalação global é prática para uso individual. O ponto fraco é que duas pessoas da equipe podem ter versões diferentes sem perceber.
Instalação vinculada ao projeto
A partir do Go 1.25, o toolchain permite registrar ferramentas no módulo:
go get -tool github.com/air-verse/air@latest
go tool air -v
Depois, o comando de desenvolvimento pode ser:
go tool air
Essa abordagem deixa a dependência de ferramenta associada ao projeto e facilita reproduzir o ambiente em CI ou no onboarding. Depois de escolher uma versão, revise e faça commit das alterações em go.mod e go.sum, como faria com qualquer ferramenta compartilhada. Para entender melhor o arquivo do módulo, consulte Go Modules na prática.
Exemplo de API para usar com Air
Considere esta estrutura:
minha-api/
├── cmd/
│ └── api/
│ └── main.go
├── internal/
├── go.mod
└── .air.toml
O servidor mínimo pode ser:
package main
import (
"log/slog"
"net/http"
"os"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"ok"}`))
})
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
slog.Info("servidor iniciado", "addr", server.Addr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("servidor encerrou com erro", "error", err)
os.Exit(1)
}
}
Confirme primeiro que o projeto funciona sem watcher:
go run ./cmd/api
curl http://localhost:8080/health
Só adicione Air depois desse teste. Isso separa erros da aplicação de erros da ferramenta.
Gerando e ajustando o .air.toml
Na raiz do projeto, execute:
air init
Ou, se Air foi registrado como tool do módulo:
go tool air init
O comando gera uma configuração completa. Para uma API em cmd/api, uma versão enxuta pode ser:
root = "."
tmp_dir = "tmp"
[build]
cmd = "go build -o ./tmp/api ./cmd/api"
entrypoint = ["./tmp/api"]
include_ext = ["go", "tpl", "tmpl", "html"]
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
delay = 300
stop_on_error = true
send_interrupt = true
kill_delay = "500ms"
[log]
time = true
[misc]
clean_on_exit = true
Os campos mais importantes são:
build.cmd: comando executado para produzir o programa;build.entrypoint: binário que Air deve iniciar depois do build;include_ext: extensões que podem disparar o ciclo;exclude_dir: diretórios que não devem ser observados;delay: debounce em milissegundos;stop_on_error: evita iniciar um binário inválido quando o build falha;send_interrupt: envia interrupção antes de matar o processo;kill_delay: dá tempo para o encerramento terminar;clean_on_exit: remove temporários quando Air é encerrado.
O campo legado build.bin aparece em exemplos antigos, mas a documentação atual recomenda build.entrypoint. Usar a forma nova também permite passar argumentos padrão:
[build]
cmd = "go build -o ./tmp/api ./cmd/api"
entrypoint = ["./tmp/api", "serve", "--addr=:8080"]
Adicione a pasta temporária ao .gitignore:
/tmp/
Não exclua todo diretório chamado tmp indiscriminadamente se sua aplicação guarda fixtures ou arquivos relevantes nele. O nome e o caminho devem refletir a estrutura real do projeto.
Executando o ciclo de desenvolvimento
Com a configuração pronta:
air
Ou:
go tool air
Altere a resposta do endpoint e salve o arquivo. Air deve mostrar o build, interromper o processo anterior e iniciar o novo binário.
Você também pode passar argumentos para a aplicação depois de --:
air -- serve --addr=:8081
Quando houver uma configuração específica:
air -c .air.dev.toml -- serve
Para investigar por que um arquivo não disparou rebuild, rode o modo de debug:
air -d
Rodar testes antes de reiniciar: vale a pena?
É possível transformar o comando de build em uma pequena pipeline:
[build]
cmd = "go test ./... && go build -o ./tmp/api ./cmd/api"
entrypoint = ["./tmp/api"]
Isso garante que a aplicação só reinicie quando toda a suíte passar. Em um projeto pequeno, o feedback é ótimo. Em um monorepo grande, executar todos os testes a cada tecla salva pode deixar o ciclo lento.
Uma divisão mais equilibrada é:
- Air executa
go builda cada alteração; - o editor roda testes do pacote atual;
- um terminal separado executa
go test ./...quando necessário; - o CI roda suíte completa, race detector e análise estática.
Não esconda todos os checks dentro de uma string difícil de ler. Se a preparação tiver várias etapas, crie um script ou alvo de Makefile:
.PHONY: dev dev-build test
dev:
go tool air
dev-build:
go test ./internal/... && go build -o ./tmp/api ./cmd/api
test:
go test -race ./...
E use:
[build]
cmd = "make dev-build"
entrypoint = ["./tmp/api"]
Para aprofundar o fluxo, veja Go Test: testing, race e cobertura e o guia de Staticcheck e golangci-lint.
Air com Templ, sqlc e go generate
Projetos modernos podem gerar arquivos Go antes do build. Se você usa Templ, sqlc, mocks ou outro gerador, não quer recompilar antes que o arquivo gerado esteja atualizado.
Air suporta regras que executam um comando para tipos específicos de arquivo. Um exemplo com Templ:
[build]
cmd = "go build -o ./tmp/api ./cmd/api"
entrypoint = ["./tmp/api"]
exclude_dir = ["tmp", "vendor"]
include_ext = ["go"]
[[build.rules]]
name = "templ"
include_ext = ["templ"]
cmd = "templ generate"
Quando um arquivo .templ muda, a regra executa templ generate. Os .go gerados entram no conjunto observado e disparam o build principal naturalmente.
Para sqlc, uma regra possível é:
[[build.rules]]
name = "sqlc"
include_dir = ["db/query", "db/migrations"]
include_ext = ["sql"]
cmd = "sqlc generate"
Evite loops: se o diretório de saída do gerador também dispara a mesma regra, cada geração pode provocar outra geração. Restrinja include_dir, include_ext e exclude_regex ao mínimo necessário.
Se seu frontend usa HTML gerado pelo Go, o tutorial de Templ com HTMX mostra como organizar componentes e handlers.
Recarregar o navegador com o proxy do Air
Reiniciar a API não atualiza automaticamente uma aba do navegador. Air oferece um proxy opcional que encaminha requests para sua aplicação e injeta um pequeno script de reload em respostas HTML.
[proxy]
enabled = true
proxy_port = 8090
app_port = 8080
app_start_timeout = 10000
Nesse caso:
- a aplicação continua ouvindo em
http://localhost:8080; - você abre
http://localhost:8090no navegador; - depois de um build bem-sucedido, o proxy recarrega a página.
O HTML precisa conter </body> para a injeção funcionar. Arquivos CSS, JavaScript e templates também precisam estar dentro das regras de observação. Para uma API JSON sem páginas HTML, o proxy costuma ser desnecessário: reiniciar o servidor já resolve o ciclo principal.
Não exponha a porta do proxy em produção. Ela existe para conveniência local.
Air com Docker Compose
Para desenvolver dentro de container, instale Air na imagem de desenvolvimento e monte o código-fonte como volume.
Dockerfile.dev:
FROM golang:1.27-alpine
WORKDIR /app
RUN go install github.com/air-verse/air@latest
COPY go.mod go.sum ./
RUN go mod download
CMD ["air", "-c", ".air.toml"]
compose.yaml:
services:
api:
build:
context: .
dockerfile: Dockerfile.dev
working_dir: /app
volumes:
- .:/app
- go-mod-cache:/go/pkg/mod
ports:
- "8080:8080"
environment:
APP_ENV: development
DATABASE_URL: postgres://app:app@db:5432/app?sslmode=disable
depends_on:
- db
db:
image: postgres:17-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
go-mod-cache:
postgres-data:
Execute:
docker compose up --build
O bind mount .:/app é essencial: sem ele, o container enxerga apenas os arquivos copiados durante o build da imagem e não recebe as alterações do host.
Essa imagem é para desenvolvimento. A imagem de produção deve compilar em um estágio separado e copiar apenas o binário, sem Air, compilador ou código-fonte. Veja o guia de Docker com Go e multi-stage build e as opções de hospedagem para APIs Go.
Rebuild não dispara no Docker Desktop ou WSL
Eventos do filesystem podem se comportar de forma diferente entre Linux nativo, Docker Desktop, WSL e volumes montados. Se Air funciona no host, mas não dentro do container:
- confirme que o arquivo alterado aparece dentro do container;
- verifique
working_dire destino do volume; - rode
air -dpara inspecionar os diretórios observados; - confira se a extensão está em
include_ext; - ative polling na configuração gerada pelo Air, quando necessário;
- evite observar caches e
node_modules, que geram ruído excessivo.
Antes de culpar o watcher, execute manualmente dentro do container:
docker compose exec api go build ./cmd/api
docker compose exec api air -d
Se o build manual falha, o problema não é live reload.
Variáveis de ambiente e segredos
Air pode carregar arquivos de ambiente configurados em env_files:
env_files = [".env.development", ".env"]
Arquivos posteriores sobrescrevem valores anteriores, mas variáveis que já existiam antes de Air iniciar não são substituídas. Isso permite definir defaults locais sem atropelar valores exportados pelo shell.
Não faça commit de senhas, tokens ou credenciais reais. Mantenha um .env.example com nomes e valores fictícios, documente as variáveis obrigatórias e use o mecanismo de secrets da plataforma fora do desenvolvimento. O guia de configuração com Viper e variáveis de ambiente ajuda a separar configuração de código.
Erros comuns e como corrigir
air: command not found
O binário provavelmente foi instalado fora do PATH:
export PATH="$PATH:$(go env GOPATH)/bin"
which air
air -v
Se o projeto usa tool dependency, execute go tool air em vez de air.
Air entra em loop de rebuild
Geralmente o binário ou outro arquivo gerado está dentro de um diretório observado. Coloque a saída em tmp, adicione esse diretório a exclude_dir e ao .gitignore.
Também procure geradores que reescrevem arquivos mesmo quando o conteúdo não mudou.
address already in use
O processo anterior não encerrou ou outra aplicação está usando a porta. Confira:
lsof -i :8080
Use send_interrupt = true e um kill_delay compatível com o graceful shutdown. Seu programa também deve parar listeners e conexões ao receber SIGINT.
A compilação funciona, mas Air inicia o binário errado
Garanta que build.cmd e build.entrypoint apontam para o mesmo artefato:
[build]
cmd = "go build -o ./tmp/api ./cmd/api"
entrypoint = ["./tmp/api"]
Caminhos relativos são resolvidos a partir de root. Evite depender do diretório em que uma IDE por acaso abriu o terminal.
Cada save demora demais
Reduza o trabalho do ciclo:
- não rode toda a suíte a cada rebuild;
- exclua
.git,vendor,tmp, caches e assets irrelevantes; - aumente
delayse o editor salva vários arquivos em sequência; - compile apenas o comando necessário;
- meça geradores como Templ, sqlc e mocks separadamente.
Go já oferece compilação rápida; uma configuração que observa milhares de arquivos ou executa cinco ferramentas serialmente pode eliminar essa vantagem.
Configuração recomendada por cenário
API pequena
Use build direto, poucas extensões e sem Docker:
[build]
cmd = "go build -o ./tmp/api ./cmd/api"
entrypoint = ["./tmp/api"]
include_ext = ["go"]
exclude_dir = ["tmp", "vendor"]
API com templates server-side
Observe Go, templates e assets relevantes; ative proxy apenas se quiser reload do navegador.
Projeto com geração de código
Use [[build.rules]] para cada gerador e mantenha o build principal simples. Não coloque templ generate, sqlc generate, mocks e testes completos em uma única linha sem medir o custo.
Monorepo
Defina root, include_dir e exclude_dir com cuidado. Se existem vários binários independentes, pode ser mais claro manter uma configuração Air por serviço do que recompilar o repositório inteiro.
Equipe com Docker
Use uma imagem de desenvolvimento separada, volume para código e cache de módulos. Fixe a versão da ferramenta quando o ambiente precisar ser reproduzível; não dependa eternamente de @latest em cada build.
Air deve entrar no CI?
Normalmente, não como watcher. CI precisa executar comandos finitos e terminar:
go test ./...
go test -race ./...
go vet ./...
go build ./cmd/api
Pode fazer sentido instalar a mesma versão do Air apenas para validar que a ferramenta ainda inicia ou que o .air.toml continua parseável, mas manter um processo observando arquivos em CI não agrega valor.
A fronteira recomendada é:
- desenvolvimento: Air acelera o feedback;
- CI: comandos explícitos validam o projeto;
- produção: binário imutável e processo supervisionado pela plataforma.
Para releases de ferramentas e serviços, veja GoReleaser, checksums e SBOM.
Checklist para adotar Air sem criar dívida
- O projeto executa corretamente sem Air.
-
build.cmdcompila apenas o binário necessário. -
build.entrypointaponta para o artefato gerado. - A pasta temporária está em
exclude_dire.gitignore. -
vendor, caches e diretórios grandes não são observados sem necessidade. - O processo responde a interrupção e encerra recursos.
- Testes lentos não bloqueiam todo save sem justificativa.
- Geradores usam regras específicas e não entram em loop.
- Docker monta o código no mesmo
working_dirusado pelo container. - Segredos locais não são versionados.
- A versão da ferramenta é reproduzível para a equipe.
- Air não faz parte da imagem ou do comando de produção.
Perguntas frequentes
O que é Air em Go?
Air é uma ferramenta de live reload. Ela observa alterações, executa o build configurado e reinicia o programa quando a compilação termina com sucesso. É especialmente útil para APIs, aplicações web server-side e serviços locais.
Como instalar Air?
Com Go 1.25+, use go install github.com/air-verse/air@latest para uma instalação global. Para associar a ferramenta ao módulo, use go get -tool github.com/air-verse/air@latest e execute go tool air.
Air funciona com Docker Compose?
Sim. Instale Air na imagem de desenvolvimento, monte o repositório como volume e mantenha working_dir alinhado ao destino do mount. Se alterações não forem detectadas, verifique eventos do filesystem e polling.
Hot reload e live reload são iguais?
No contexto do Air, o programa é recompilado e reiniciado. Não existe substituição de código dentro do processo antigo. Para HTML, o proxy opcional também consegue recarregar a página no navegador.
Posso usar Air em produção?
Não é recomendado. Produção deve executar um binário compilado, versionado e imutável sob um supervisor ou orquestrador. Observar código-fonte e recompilar no servidor aumenta a superfície de falha.
Conclusão
Air reduz o atrito diário do desenvolvimento Go sem invadir o código da aplicação. A configuração boa é pequena: um comando de build explícito, um entrypoint correto, uma pasta temporária excluída e apenas os arquivos necessários sob observação.
Comece com air init, simplifique o .air.toml para a estrutura real do projeto e confirme que cada etapa também funciona manualmente. Depois, se houver necessidade, adicione regras para Templ ou sqlc, proxy para o navegador e Docker Compose. Evite transformar o watcher em uma pipeline gigantesca ou levar a ferramenta para produção.
Para continuar, revise API REST com Go, graceful shutdown, testes em Go e Docker com Go. A combinação de reload rápido, testes focados e um build de produção simples cria um fluxo local confortável sem comprometer a previsibilidade do deploy.