Simultaneidade e fechamento gracioso do cliente MDB
Jorge D. Ortiz-Fuentes5 min read • Published Sep 04, 2024 • Updated Sep 04, 2024
APLICATIVO COMPLETO
Avalie esse Tutorial
No artigo anterior e no vídeo correspondente, aprenderam a persistir os dados que foram trocados com nosso servidor HTTP usando o MongoDB. Usamos o driver do MongoDB para Go para acessar um MongoDB Atlas clustergratuito e usar instâncias de nossos dados diretamente com ele.
Neste artigo, vamos nos concentrar em um tópico mais avançado que muitas vezes é ignorado: como desligar corretamente nosso servidor. Isso pode ser usado com o
WaitGroups
fornecido pelo pacotesync
, mas decidi fazê-lo usando goroutines e canais para abordá-los em um caso de uso mais realista, mas compreensível.Na versão mais recente do código desse programa, definimos uma maneira de fechar adequadamente a conexão com o banco de dados. No entanto, não tinhamos como parar normalmente o servidor da web. Usar Control+C fechou o servidor imediatamente e esse código nunca foi executado.
- Antes de podermos personalizar a forma como nosso servidor HTTP é encerrado, precisamos organizar a forma como ele é construído. Primeiro, as rotas que criamos são adicionadas ao
DefaultServeMux
. Em vez disso, podemos criar nosso próprio roteador e adicionar rotas a ele (em vez das antigas).1 router := http.newservemux() 2 router.handlefunc("get /", func(w http.responsewriter, r *http.request) { 3 w.write([]byte("HTTP caracola")) 4 }) 5 router.handlefunc("post /notes", createNote) - O roteador que acabamos de criar, juntamente com outros parâmetros de configuração, pode ser usado para criar um
http.Server
. Outros parâmetros também podem ser definidos: Leia a documentação deste.1 server := http.Server{ 2 Addr: serverAddr, 3 Handler: router, 4 } - Use esse servidor para escutar as conexões, em vez do servidor padrão. Aqui, não precisamos de parâmetros na função porque eles são fornecidos com a instância
server
e estamos invocando um de seus métodos.1 log.Fatal(server.ListenAndServe()) - Se você compilar e executar esta versão, ela deverá se comportar exatamente da mesma forma que antes.
- A função
ListenAndServe()
retorna um erro específico quando o servidor é fechado comShutdown()
. Vamos lidar com isso separadamente.1 if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { 2 log.Fatalf("HTTP server error %v\n", err) 3 }
- O tipo
Server
tem outros métodos que podemos usar. Entre outros, podemos definir a função que será executada apósShutdown()
ser invocado. Isso deve ser feito antes queListenAndServe()
seja invocado.1 server.RegisterOnShutdown(func() { 2 fmt.Println("Signal shutdown") 3 }) - Em seguida, definimos uma função anônima que aguarda o sinal de interrupção e inicia o desligamento adequado do servidor. Começamos com uma função vazia.
1 func() { 2 } - O Go lida com sinais POSIX usando
signal.Notify()
. Essa função usa um canal que será usado para notificar e o sinal que você deseja que seja tratado. Um canal é como um pipe no Go com um tipo associado que é definido quando o canal é criado. Os dados são enviados para um canal usando esta notação:channel <- data
. E é lido usando esta outra notação:data <- channel
. Se você ler de um canal que não tem dados, o "thread de execução" atual será interrompido e aguardará que os dados estejam disponíveis. Se você escrever dados em um canal, o "thread de execução" atual será interrompido e aguardará que os dados sejam lidos. Devido a esse comportamento específico, eles são comumente usados como mecanismo de sincronização. Os canais também podem ter um buffer de tamanho fixo. A gravação em um canal não bloqueia até que o buffer esteja cheio. Vamos criar o canal que comunica sinais (os.Signal
) com um buffer de um elemento e usá-lo com a função para lidar com o sinal.1 sigint := make(chan os.Signal, 1) 2 signal.Notify(sigint, os.Interrupt) - A leitura deste canal aguardará até que um sinal de interrupção (Control+C) seja recebido.
1 <-sigint - E quando isso acontecer, poderemos iniciar o processo de desligamento. Se recebermos um erro, vamos registrá-lo e
panic
. Poderíamos (deveríamos?) ter um tempo limite nesse contexto.1 if err := server.Shutdown(context.Background()); err != nil { 2 log.Fatalf("Server shutdown error: %v", err) 3 } - Agora que definimos a função anônima, colocando parênteses no final, invocamos a função. Entretanto, se fizermos isso, essa função será executada no thread de execução "atual" e nosso programa aguardará o sinal e encerrará o servidor sem sequer iniciá-lo. Precisamos criar outro thread de execução "" . Felizmente, isso é trivial em Go: você pode criar outra thread de execução "" usando a palavra-chave
go
antes de executar uma função. Isso é chamado de goroutine.1 go func() { 2 // ... 3 }()
- Se executarmos esta versão do programa, ela deverá funcionar bem. No entanto, há uma ressalva. Quando
server.Shutdown()
for invocado, o servidor parará de ouvir e sairá. Ele também executará a função que registramos comRegisterOnShutdown()
em outra goroutine. E, dependendo da ordem de execução e do comprimento da função registrada, ela pode sairmain
antes que a função registrada termine seu trabalho. Quando o programa sai da função principal, quaisquer outras goroutines são canceladas. Podemos usar outro canal para evitar que isso aconteça. Criamos este novo canal sem dados (estrutura vazia), pois ele serve apenas para sincronização.1 done := make(chan struct{}) - Vamos ler deste canal logo antes de sair da função
main
. Se ainda não tivermos escrito para ele, bloquearemos lá.1 <-done - Quando começamos a executar a função que será executada no desligamento, adiamos a gravação nesse canal, garantindo que será a última coisa a ser feita quando a função for concluída, desbloqueando o fim da execução do programa.
1 defer func(){ 2 done<-struct{}{} 3 }() - Vamos adicionar algum atraso à função para verificar se isso está funcionando.
1 time.Sleep(5 * time.Second) - Isso deve resolver a situação. Compile e teste.
- No entanto, se o servidor falhar devido a algum erro, ele permanecerá lá, esperando que o canal
done
seja gravado. Uma maneira de resolver isso é fechar o canal, pois a leitura de um canal fechado não bloqueia. A outra é usar a função de registro adequada para trigger um pânico quando o erro for detectado.log.Fatal()
imprime e usaos.Exit()
, enquantolog.Panic()
imprime uma mensagem e aciona um pânico, que faz com que funções adiadas sejam executadas.
Com este artigo final, abordamos:
- As possibilidades de configuração oferecidas pelo servidor HTTP da biblioteca padrão.
- A criação de goroutines que permitem a execução simultânea de código.
- Uso de canais para sincronização de goroutines.
O repositório tem todo o código desta série para que você possa acompanhar. Os tópicos abordados são os fundamentos que você precisa conhecer para produzir REST API completas, servidores back-end ou até mesmo microsserviços escritos em Go. A caminho está à sua frente e estamos ansiosos para aprender o que você criará com esse conhecimento.
Mantenha-se atento. Hackeie seu código. Até a próxima!
Principais comentários nos fóruns
Ainda não há comentários sobre este artigo.