Encerramento seguro
O encerramento seguro (graceful shutdown) é uma forma de finalizar um programa sem interromper bruscamente as operações que estão sendo executadas. Quando encerramos um programa, principalmente aqueles que trabalham com goroutines, conexões de rede ou arquivos, podemos ter tarefas que ainda não terminaram. Se o programa for encerrado imediatamente, essas operações poderão ser interrompidas antes de concluir seu trabalho.
Em Go, podemos controlar esse encerramento usando os packages os/signal, context e sync, permitindo receber sinais do sistema operacional, avisar as goroutines que devem parar e aguardar sua finalização.
Capturando sinais do sistema
O Linux e outros sistemas operacionais possuem sinais (signals), que são notificações enviadas aos processos para informar determinados acontecimentos, como uma solicitação de encerramento. Por exemplo, quando pressionamos Ctrl+C no terminal, normalmente é enviado o sinal SIGINT para o programa. Já o SIGTERM é usado para solicitar que um processo seja encerrado, sendo comum em servidores e serviços.
Em Go, podemos capturar esses sinais usando o package os/signal.
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
// Cria um contexto que será cancelado ao receber um sinal.
ctx, parar := signal.NotifyContext(
context.Background(),
os.Interrupt,
syscall.SIGTERM,
)
defer parar()
fmt.Println("Programa iniciado")
fmt.Println("Pressione Ctrl+C para encerrar")
// Aguarda um sinal do sistema operacional.
<-ctx.Done()
fmt.Println("Sinal recebido, encerrando programa...")
}
Resultado possível:
Programa iniciado
Pressione Ctrl+C para encerrar
^C
Sinal recebido, encerrando programa...
O signal.NotifyContext() cria um contexto que será cancelado automaticamente quando o programa receber um dos sinais informados. Se nenhum dos sinais informados for recebido, o contexto continuará ativo normalmente, sem ser cancelado. O os.Interrupt representa uma interrupção, como aquela gerada pelo Ctrl+C, enquanto syscall.SIGTERM representa uma solicitação de encerramento, comum em sistemas Unix, como Linux e macOS.
Quando o sinal é recebido, o contexto é cancelado e o channel retornado por ctx.Done() é fechado, permitindo que o programa continue sua execução e finalize normalmente. Também usamos defer parar() para liberar os recursos associados ao recebimento dos sinais.
Encerrando goroutines
Quando trabalhamos com goroutines, precisamos lembrar que encerrar a função main() também encerra o programa, mesmo que outras goroutines ainda estejam executando. Por isso, podemos combinar context e sync.WaitGroup para avisar as goroutines que devem parar e aguardar até que todas tenham terminado.
package main
import (
"context"
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
func executarTarefa(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
// Executa uma tarefa a cada segundo.
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
fmt.Println("Executando tarefa...")
// Verifica se o contexto foi cancelado.
case <-ctx.Done():
fmt.Println("Goroutine encerrada")
return
}
}
}
func main() {
// Aguarda sinais de encerramento.
ctx, parar := signal.NotifyContext(
context.Background(),
os.Interrupt,
syscall.SIGTERM,
)
defer parar()
var wg sync.WaitGroup
wg.Add(1)
// Inicia a goroutine.
go executarTarefa(ctx, &wg)
fmt.Println("Programa iniciado. Pressione Ctrl+C para encerrar.")
// Aguarda o sinal de encerramento.
<-ctx.Done()
fmt.Println("Aguardando as goroutines terminarem...")
// Aguarda a goroutine finalizar.
wg.Wait()
fmt.Println("Programa encerrado com segurança!")
}
Resultado possível:
Programa iniciado. Pressione Ctrl+C para encerrar.
Executando tarefa...
Executando tarefa...
Executando tarefa...
^C
Aguardando as goroutines terminarem...
Goroutine encerrada
Programa encerrado com segurança!
Aqui criamos uma goroutine que executa uma tarefa a cada segundo. Para controlar sua execução, usamos um select que verifica tanto o tempo definido pelo ticker quanto o cancelamento do contexto. Quando pressionamos Ctrl+C, o sistema operacional envia o sinal de interrupção, fazendo com que o contexto seja cancelado.
A goroutine identifica esse cancelamento por meio de ctx.Done() e executa return, encerrando sua função. Enquanto isso, a função main() permanece aguardando a conclusão da goroutine por meio de wg.Wait(). Isso permite que o programa finalize somente depois que a goroutine terminar.
O context não encerra automaticamente uma goroutine, ele apenas envia um sinal de cancelamento. Por isso, cada goroutine precisa verificar esse sinal e encerrar sua própria execução. Também precisamos lembrar que esse comportamento depende da forma como o programa foi encerrado. No Linux, por exemplo, o sinal SIGKILL não pode ser capturado, então não conseguimos executar esse processo de encerramento seguro quando ele é recebido.
Organização de ferramentas CLI
Uma ferramenta CLI (Command Line Interface) é um programa que permite executar operações por meio de comandos no terminal. Em Go, podemos criar ferramentas CLI que recebem argumentos, executam tarefas e retornam resultados diretamente pelo terminal. Por exemplo, imagine um programa que aceita comandos para iniciar uma operação ou mostrar sua versão.
package main
import (
"fmt"
"os"
)
func main() {
// Verifica se algum comando foi informado.
if len(os.Args) < 2 {
fmt.Println("Informe um comando")
return
}
// Obtém o comando informado.
comando := os.Args[1]
switch comando {
case "executar":
fmt.Println("Executando tarefa...")
case "versao":
fmt.Println("Versão 1.0")
default:
fmt.Println("Comando desconhecido")
}
}
Nesse exemplo, usamos os.Args, que contém os argumentos recebidos pelo programa durante sua execução. O primeiro elemento, os.Args[0], representa o caminho usado para iniciar o programa, enquanto os próximos representam os argumentos informados. Depois de compilar:
go build -o ferramenta .
Podemos executar:
./ferramenta executar
Ou consultar a versão:
./ferramenta versao
Conforme uma ferramenta CLI cresce, podemos separar suas funcionalidades em diferentes packages, mantendo a função main() responsável principalmente por receber os comandos e iniciar as operações.
projeto/
│
├── go.mod
│
├── cmd/
│ └── ferramenta/
│ └── main.go
│
└── internal/
└── tarefas/
└── tarefas.go
Nesse exemplo, o diretório cmd/ferramenta contém o ponto de entrada do programa, enquanto internal/tarefas guarda as funcionalidades relacionadas à execução das tarefas. O diretório internal possui um comportamento especial em Go, impedindo que packages fora da árvore permitida importem diretamente seu conteúdo. Isso ajuda a manter partes internas do projeto separadas das funcionalidades destinadas a outros módulos.
Essa organização não é obrigatória. Para ferramentas pequenas, podemos manter tudo em um único main.go e separar o código apenas quando houver necessidade. O encerramento seguro também pode fazer parte dessas ferramentas, principalmente quando executam operações demoradas, como downloads, processamento de arquivos ou conexões de rede. Nesses casos, podemos capturar sinais do sistema operacional e aguardar a finalização das tarefas antes de encerrar o programa.