É possível interceptar Ctrl+C em Go e executar uma limpeza, como o defer? Análise sob o capô
\n\nVocê já iniciou um servidor em Go, pressionou Ctrl+C e pensou: "E se, antes de sair, eu pudesse fechar cuidadosamente as conexões com o banco de dados, salvar o cache ou simplesmente dizer 'Tchau!'?" A pergunta é tão popular no Stack Overflow (milhares de visualizações!) que decidimos analisá-la detalhadamente. Hoje, não vamos apenas dar uma receita pronta, mas vamos olhar sob o capô — como o Go lida com sinais, o que é signal.Notify e por que o defer não está totalmente envolvido aqui.
Prontos? Vamos lá!
\n\nProblema: Ctrl+C mata o programa instantaneamente
\n\nPor padrão, quando você pressiona Ctrl+C no terminal, o SO envia o sinal SIGINT ao processo. O programa Go, se nada for feito, termina abruptamente — todas as goroutines são interrompidas, as funções defer não são executadas. É como puxar o plugue da tomada: o computador desliga, mas os dados podem ser perdidos.
Exemplo clássico: você escreve um servidor web, abre uma conexão com o PostgreSQL e, de repente — Ctrl+C. Se o sinal não for tratado, a conexão ficará pendente e o pool de conexões não será liberado. Após algumas reinicializações assim, o banco de dados dirá: "Desculpe, não aguento mais".
\n\npackage main\n\nimport (\n "fmt"\n "os"\n "os/signal"\n "syscall"\n)\n\nfunc main() {\n // ... seu código\n fmt.Println("Pressione Ctrl+C para finalizar")\n // Aguarda o sinal\n sig := make(chan os.Signal, 1)\n signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)\n <-sig\n fmt.Println("Sinal recebido, finalizando...")\n}\n\nEste código já captura o sinal, mas e se precisarmos executar uma limpeza complexa? É aí que a mágica começa.
\n\nComo funciona sob o capô: signal.Notify e canais
\n\nA função signal.Notify registra seu canal para receber sinais específicos. Quando o SO envia um sinal, o runtime do Go o coloca no canal. Se o canal for bufferizado (e fizemos um buffer de 1), o sinal não será perdido, mesmo que a goroutine receptora esteja ocupada.
Uma nuance importante: signal.Notify intercepta o sinal, mas não bloqueia sua propagação. Se você não processar o sinal no canal, o programa ainda será encerrado após a execução dos defer (se houverem no main). Mas se você quiser executar uma limpeza — é necessário aguardar sua conclusão.
Esquema de funcionamento:
\n- \n
- SO → envia SIGINT ao processo \n
- Runtime Go → coloca o sinal no canal registrado via
signal.Notify\n - Sua goroutine → lê do canal e inicia a limpeza \n
- Após a limpeza → você chama
os.Exit(0)ou simplesmente sai do main \n
Mas como fazer a limpeza "no estilo defer"? Afinal, o defer é executado ao retornar da função, não ao receber um sinal. Resposta: usamos defer + bloqueio do main até receber o sinal.
Receita: limpeza "tipo defer" ao pressionar Ctrl+C
\n\nA ideia é simples: escrevemos funções de limpeza que devem ser executadas ao finalizar e as "penduramos" no defer. Em seguida, bloqueamos o main até receber o sinal. Quando o sinal chega, o main é finalizado e todos os defer são acionados. Elegante, não?
package main\n\nimport (\n "fmt"\n "os"\n "os/signal"\n "syscall"\n "time"\n)\n\nfunc cleanup() {\n fmt.Println("Limpeza: fechando conexões...")\n time.Sleep(2 * time.Second) // simulação de trabalho\n fmt.Println("Limpeza concluída")\n}\n\nfunc main() {\n // defer será executado em qualquer finalização do main\n defer cleanup()\n\n // Canal para o sinal\n sig := make(chan os.Signal, 1)\n signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)\n\n fmt.Println("Servidor iniciado. Pressione Ctrl+C para parar.")\n fmt.Println("Executando limpeza...")\n\n //