Voltar ao blog
9 de maio de 20267 min de leitura

WebSockets em Go na Prática - Streaming de Dados, Reconnect e Fan-out com Goroutines!

gowebsocketrealtimeprogramming

Dez criptomoedas. Atualizações a cada segundo. Salvar no banco, verificar alertas de preço e repassar para todos os clientes conectados no frontend ao mesmo tempo. Quando montei esse pipeline pela primeira vez, a conexão com a Binance caía a cada 2 horas e o sistema simplesmente parava de receber dados sem ninguém perceber. O preço no frontend congelava e só descobríamos quando um usuário reclamava. O problema não era abrir a conexão WebSocket, isso qualquer tutorial ensina. O problema era manter ela viva, reconectar quando cai, e distribuir os dados para múltiplos consumidores sem bloquear o fluxo.

Guia de tópicos:

  • O Cenário: Dados que Não Podem Esperar
  • WebSocket vs Polling: Por Que Conexão Persistente
  • Consumindo WebSocket de API Externa (Binance)
  • Reconnect com Backoff Exponencial: Resiliência na Prática
  • Fan-out Pattern: Um Produtor, N Consumidores
  • WebSocket para o Frontend: Repassando Dados em Tempo Real
  • Channels e Goroutines: A Cola que Junta Tudo
  • Considerações Finais
  • Links Indicativos

O Cenário: Dados que Não Podem Esperar

Imagina que você tem uma aplicação que monitora preços de criptomoedas. O preço do Bitcoin muda várias vezes por segundo. Se você usar polling (fazer um GET a cada 5 segundos), vai ter dados desatualizados, vai desperdiçar banda com requests repetidos, e vai sobrecarregar a API do provedor com milhares de chamadas desnecessárias. Além disso, se você tem 10 criptomoedas para monitorar, são 10 requests a cada 5 segundos. Em um dia, são 172.800 requests HTTP. Isso é ineficiente por qualquer ângulo que você olhe.

A alternativa é usar WebSocket: você abre uma conexão persistente com o provedor de dados, e ele te envia atualizações conforme acontecem. Uma única conexão TCP, sem overhead de HTTP headers a cada mensagem, sem polling desnecessário. O dado chega no instante em que é gerado.

No projeto que uso como referência, a Binance oferece WebSocket para streaming de trades em tempo real. Você conecta uma vez, informa quais pares quer monitorar (BTCUSDT, ETHUSDT, etc.), e recebe eventos de trade conforme acontecem. Cada evento tem o symbol, o preço e o volume da transação.

WebSocket vs Polling: Por Que Conexão Persistente

Antes de mergulhar na implementação, vale entender quando WebSocket faz sentido e quando polling resolve.

Polling faz sentido quando: os dados mudam com pouca frequência (a cada minutos ou horas), quando você não precisa de latência baixa, ou quando o provedor não oferece WebSocket. Nesse caso, um cron job que faz GET a cada X minutos é simples, confiável e suficiente.

WebSocket faz sentido quando: os dados mudam com alta frequência (segundos ou menos), quando latência importa (o usuário precisa ver o preço atualizado imediatamente), ou quando o volume de dados é alto e o overhead de HTTP se torna significativo.

No caso de preços de criptomoedas, WebSocket é a escolha óbvia. O preço muda várias vezes por segundo, o usuário quer ver atualização instantânea, e monitorar 10 pares via polling geraria um volume absurdo de requests.

Consumindo WebSocket de API Externa (Binance)

Quando você consome um WebSocket de uma API externa, você é o cliente. Você abre a conexão, recebe dados, e processa. Parece simples, mas tem nuances importantes.

Primeiro: cada par de criptomoeda precisa de sua própria conexão WebSocket (no caso da Binance). Se você monitora 10 pares, são 10 conexões simultâneas. Cada uma rodando em sua própria goroutine, recebendo dados independentemente.

Segundo: os dados chegam em formato JSON com campos como symbol, price e quantity. Você precisa fazer parse, converter tipos (o preço vem como string, não como float), e decidir o que fazer com cada evento. No projeto, cada evento de trade atualiza um mapa em memória com o último preço de cada crypto.

Terceiro: você não quer processar cada evento individualmente. Se o BTC tem 50 trades por segundo, você não precisa salvar 50 registros por segundo no banco. A estratégia é acumular em memória e fazer flush a cada intervalo (no projeto, a cada 10 segundos). Isso reduz a carga no banco de 50 writes/segundo para 1 write a cada 10 segundos, sem perder o dado mais recente.

A concorrência aqui é gerenciada com um mutex: múltiplas goroutines (uma por par) escrevem no mapa de preços, e um ticker periódico lê o mapa e envia para o channel de saída. O mutex garante que leitura e escrita não acontecem simultaneamente.

Reconnect com Backoff Exponencial: Resiliência na Prática

Aqui é onde a maioria dos tutoriais para. Eles te mostram como conectar, mas não o que fazer quando a conexão cai. E ela vai cair. A Binance faz manutenção, a rede oscila, o servidor reinicia. Se sua aplicação não reconecta automaticamente, ela simplesmente para de receber dados e ninguém percebe até um usuário reclamar.

A estratégia de reconnect com backoff exponencial funciona assim: quando a conexão cai, você tenta reconectar imediatamente. Se falhar, espera 1 segundo e tenta de novo. Se falhar de novo, espera 2 segundos. Depois 4, 8, 16, até um máximo (no projeto, 60 segundos). Quando consegue reconectar, reseta o contador e volta ao normal.

Isso evita dois problemas: o primeiro é ficar bombardeando o servidor com tentativas de reconexão quando ele está fora (o que pode até te banir por rate limiting). O segundo é esperar tempo demais para reconectar quando foi só um glitch momentâneo de rede.

No projeto, cada conexão WebSocket roda em uma goroutine dedicada que tem um loop infinito: conecta, espera o channel de done (que indica que a conexão fechou), loga que desconectou, espera 2 segundos, e tenta conectar de novo. Se a conexão falha no handshake, aplica o backoff exponencial. Se conectou e depois caiu, reseta o backoff porque provavelmente foi um problema temporário.

Um detalhe importante: o número máximo de tentativas existe para evitar que a goroutine fique tentando eternamente se o endpoint mudou ou foi desativado. No projeto, são 10 tentativas máximas antes de desistir e logar um erro fatal.

Fan-out Pattern: Um Produtor, N Consumidores

Agora que você tem dados chegando em tempo real da Binance, precisa distribuí-los. No projeto, os dados de preço precisam ir para três lugares: o banco de dados (InfluxDB, para histórico), o sistema de alertas (para verificar se algum preço atingiu o threshold do usuário), e os clientes WebSocket do frontend (para atualizar a interface em tempo real).

Isso é o fan-out pattern: um produtor (a conexão com a Binance) alimenta N consumidores (banco, alertas, frontend). A implementação em Go é natural com channels: o produtor escreve em um channel, e múltiplas goroutines leem desse channel ou de channels derivados.

No projeto, o fluxo é: a goroutine da Binance acumula preços em um mapa e a cada 10 segundos envia os dados para um channel. Uma goroutine consome esse channel e salva no InfluxDB. Outra goroutine (que poderia existir) verificaria os alertas. E o controller de WebSocket do frontend faz broadcast para todos os clientes conectados.

A chave aqui é que o produtor não sabe quantos consumidores existem e não bloqueia se um consumidor está lento. O channel tem buffer (no projeto, 100 itens), e se ficar cheio, o produtor descarta o dado mais antigo em vez de bloquear. Isso garante que um consumidor lento (por exemplo, o banco demorando para escrever) não trava o fluxo inteiro.

WebSocket para o Frontend: Repassando Dados em Tempo Real

Até agora falamos de consumir WebSocket (você como cliente da Binance). Agora vem a outra ponta: servir WebSocket (você como servidor para o frontend).

O conceito é diferente: aqui você mantém uma lista de clientes conectados e faz broadcast de dados para todos eles. Quando um cliente novo conecta, você adiciona na lista. Quando desconecta, remove. Quando tem dado novo, itera na lista e envia para cada um.

No projeto, o controller de WebSocket mantém um mapa de conexões ativas protegido por um RWMutex. O endpoint é um GET /ws/crypto que faz upgrade da conexão HTTP para WebSocket. Uma goroutine dedicada fica lendo de um channel de broadcast e enviando para todos os clientes.

Pontos importantes nessa implementação: primeiro, o upgrade de HTTP para WebSocket precisa de um Upgrader configurado com CheckOrigin que aceita as origens do seu frontend (ou todas, se for desenvolvimento). Segundo, quando um write para um cliente falha (porque ele desconectou), você precisa remover ele da lista e fechar a conexão. Terceiro, o broadcast não pode bloquear: se um cliente está com a conexão lenta, você não pode esperar ele receber para enviar para o próximo. Por isso o channel de broadcast tem buffer e o envio usa select com default para não bloquear.

A goroutine de leitura do cliente (que fica em loop lendo mensagens) serve principalmente para detectar desconexão. Quando o ReadMessage retorna erro, significa que o cliente saiu, e você limpa os recursos.

Channels e Goroutines: A Cola que Junta Tudo

O que faz tudo isso funcionar de forma elegante em Go são channels e goroutines. Cada conexão WebSocket com a Binance é uma goroutine. O ticker que faz flush dos dados é outra goroutine. O broadcast para os clientes do frontend é outra. O salvamento no banco é outra. São dezenas de goroutines rodando simultaneamente, comunicando-se via channels.

A regra de ouro é: não compartilhe memória comunicando, comunique compartilhando memória. Na prática, isso significa: em vez de ter uma variável global que todo mundo lê e escreve (com mutex em todo lugar), use channels para passar dados entre goroutines. Cada goroutine é dona dos seus dados e só se comunica via channel.

No projeto, o mapa de preços é a exceção: ele é compartilhado entre as goroutines de WebSocket (que escrevem) e o ticker (que lê). Nesse caso, o mutex é necessário e aceitável porque a alternativa (um channel por par de crypto) seria mais complexa sem benefício real.

O channel de dados entre o produtor (Binance) e os consumidores (banco, broadcast) é buffered com 100 itens. Isso absorve picos de dados sem bloquear o produtor. Se o buffer encher (consumidor muito lento), o produtor descarta com select/default em vez de travar.

Considerações Finais

No enfrentamento da complexidade de sistemas real-time, WebSocket é a ferramenta certa quando você precisa de dados com baixa latência e alta frequência. Mas abrir uma conexão é a parte fácil. O desafio real está na resiliência (reconnect automático), na distribuição (fan-out para múltiplos consumidores), e na gestão de recursos (não bloquear, não vazar goroutines, não acumular memória).

O que eu quero que você leve deste artigo é que um sistema real-time em Go se resume a: goroutines para concorrência, channels para comunicação, mutex quando compartilhar memória é inevitável, e backoff exponencial para resiliência. Esses 4 conceitos cobrem 90% dos cenários.

E lembre-se: nem todo dado precisa ser real-time. O preço atual da crypto? Sim, o usuário quer ver instantâneo. O volume 24h? Pode atualizar a cada minuto. O histórico de 30 dias? Pode ser uma query normal. Use real-time onde faz diferença para o usuário, não em todo lugar só porque é tecnicamente possível.


Links indicativos: