Antes de Começar: Concorrência vs Paralelismo vs Assíncrono
Essas três palavrinhas são frequentemente confundidas, especialmente se você vem do JavaScript. Vamos descomplicar:
Concorrência é quando você ORGANIZA seu programa para lidar com várias tarefas. Não significa que rodam ao mesmo tempo, mas que o programa sabe alternar entre elas. É sobre ORQUESTRAÇÃO.
Paralelismo é quando várias tarefas REALMENTE rodam ao mesmo tempo, em processadores diferentes. É sobre EXECUÇÃO SIMULTÂNEA.
Código assíncrono (como no JavaScript) é quando você escreve código que não bloqueia. Você chama uma função, ela retorna uma Promise, e você continua fazendo outras coisas enquanto espera. Mas na prática, tudo roda em uma ÚNICA thread.
A diferença prática?
Em JavaScript (Node.js), quando você faz:
await fetch('api1')
await fetch('api2')
await fetch('api3')
Você está sendo assíncrono, mas não paralelo. As requisições não rodam simultaneamente - o event loop vai alternando entre elas.
Em Go, quando você faz:
go buscarAPI1()
go buscarAPI2()
go buscarAPI3()
Você está sendo concorrente. E a mágica é: dependendo do seu processador, essas goroutines podem rodar em paralelo (simultaneamente em diferentes núcleos) ou o Go pode simplesmente alternar entre elas.
O melhor de tudo? Você não precisa se preocupar com isso. O runtime do Go cuida de tudo pra você!
O Problema Real
Quando você começa com Go, logo bate aquela dúvida: "Como faço para executar várias tarefas ao mesmo tempo sem criar uma zona?"
Em linguagens como Python ou Node.js, você pode se perder em callbacks, Promises ou async/await. Em Go, a resposta é elegantemente simples: goroutines e channels.
Mas antes do código, vamos entender o problema que essas ferramentas resolvem.
Imagine que você está construindo um sistema que precisa buscar dados de várias APIs ao mesmo tempo. Sem concorrência, seria assim:
// Sequencial - muito lento
resultado1 := buscarDadosAPI1() // 2 segundos
resultado2 := buscarDadosAPI2() // 2 segundos
resultado3 := buscarDadosAPI3() // 2 segundos
// Total: 6 segundos 😴
Com concorrência, você pode fazer tudo simultaneamente. Vou mostrar o código real agora:
// Concorrente - muito mais rápido!
resultado := make(chan string, 3)
go func() { resultado <- buscarDadosAPI1() }()
go func() { resultado <- buscarDadosAPI2() }()
go func() { resultado <- buscarDadosAPI3() }()
// Todas as APIs são chamadas ao mesmo tempo
res1 := <-resultado // ~2 segundos
res2 := <-resultado // ~2 segundos
res3 := <-resultado // ~2 segundos
// Total: ~2 segundos! 🚀
Essa é a mágica que vamos desvendar!
O que é uma Goroutine?
Uma goroutine é basicamente uma função que executa de forma independente das outras. É super leve, eficiente e gerenciada pelo próprio Go.
A sintaxe é tão simples que chega a ser engraçada:
go minhaFuncao()
É só colocar go na frente da chamada da função. Pronto! Ela vai rodar em paralelo.
Mas aí vem a pergunta: como você sabe quando a goroutine terminou? Como pega o resultado dela?
É aí que entram os channels!
Channels: A Comunicação Entre Goroutines
Um channel é como um tubo de comunicação entre goroutines. Uma goroutine coloca dados dentro, outra goroutine pega dados de fora. É seguro, sincronizado e evita aqueles bugs malucos de concorrência.
À partir daqui os exemplos dados são baseados no uso de Data-feeds da chainlink, então se você não sabe o que é isso ou quiser ter mais contexto, eu explico aqui 😉
Vamos criar um exemplo prático:
package main
import (
"context"
"fmt"
"log"
"math/big"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/smartcontractkit/chainlink/v2/core/gethwrappers/generated/aggregator_v3_interface"
)
// TokenConfig guarda as informações de cada token que queremos monitorar
type TokenConfig struct {
Symbol string
Address common.Address
}
// FormattedPrice armazena o preço formatado e outras infos úteis
type FormattedPrice struct {
Symbol string
Price *big.Float
Timestamp uint64
Decimals uint8
}
Agora vamos para a função main. Aqui nos conectamos à rede Ethereum e definimos quais tokens queremos monitorar. Sinta-se à vontade para adicionar ou remover tokens!
func main() {
// Conectando a um nó da Ethereum
client, err := ethclient.Dial("https://ethereum-rpc.publicnode.com")
if err != nil {
log.Fatalf("Ops! Deu ruim ao conectar na Ethereum: %v", err)
}
defer client.Close()
// Nossa lista de tokens para monitorar! Adicione os que quiser aqui.
tokens := []TokenConfig{
{Symbol: "ETH", Address: common.HexToAddress("0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419")},
{Symbol: "BTC", Address: common.HexToAddress("0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c")},
{Symbol: "LINK", Address: common.HexToAddress("0x2c1d072e956AFFC0D435Cb7AC38EF18d24d9127c")},
}
// Este canal vai receber os preços conforme forem sendo encontrados
pricesChan := make(chan FormattedPrice, len(tokens))
// Um contexto com timeout para não ficarmos esperando eternamente
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Lança uma goroutine para cada token - todas rodam ao mesmo tempo!
for _, token := range tokens {
go fetchTokenPrice(ctx, client, token, pricesChan)
}
// Agora é só coletar os resultados que forem chegando
fmt.Println("🎯 Buscando preços em paralelo...")
for i := 0; i < len(tokens); i++ {
select {
case price := <-pricesChan:
fmt.Printf("✅ %s: $%.2f (Atualizado: %d)\n",
price.Symbol, price.Price, price.Timestamp)
case <-ctx.Done():
log.Fatalf("⏰ Timeout! Demorou demais: %v", ctx.Err())
}
}
fmt.Println("🎉 Todos os preços foram coletados!")
}
A função fetchTokenPrice é onde a mágica acontece - ela interage com o contrato na blockchain:
// fetchTokenPrice é nossa trabalhadora: busca o preço de um token específico
func fetchTokenPrice(ctx context.Context, client *ethclient.Client, token TokenConfig, pricesChan chan<- FormattedPrice) {
// Cria uma instância do contrato para podermos conversar com ele
aggregator, err := aggregator_v3_interface.NewAggregatorV3Interface(token.Address, client)
if err != nil {
log.Printf("❌ Erro no contrato do %s: %v", token.Symbol, err)
return
}
// Pega os dados mais recentes do oráculo
roundData, err := aggregator.LatestRoundData(nil)
if err != nil {
log.Printf("❌ Erro buscando dados do %s: %v", token.Symbol, err)
return
}
// Descobre quantas casas decimais o preço tem
decimals, err := aggregator.Decimals(nil)
if err != nil {
log.Printf("❌ Erro nas casas decimais do %s: %v", token.Symbol, err)
return
}
// Converte o preço para um formato que a gente entende
price := new(big.Float).SetInt(roundData.Answer)
divisor := new(big.Float).SetInt(new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(decimals)), nil))
price.Quo(price, divisor)
// Manda o resultado pelo canal
pricesChan <- FormattedPrice{
Symbol: token.Symbol,
Price: price,
Timestamp: roundData.UpdatedAt.Uint64(),
Decimals: decimals,
}
}
O que está rolando aqui?
- Criamos um
channelchamadopricesChan - Lançamos várias goroutines, cada uma busca um token diferente
- Cada goroutine manda seu resultado pelo channel
- Na
main, vamos coletando os resultados conforme chegam
A parte <-pricesChan espera até chegar um valor no channel. Assim o programa não termina antes de todas as goroutines terminarem.
Padrão Prático: Timeout com Context
Na vida real, você não quer que seu programa fique esperando eternamente. Quer um tempo limite. É aí que o context salva:
package main
import (
"context"
"fmt"
"time"
)
func buscarDado(ctx context.Context, id int, resultado chan string) {
// Simula um trabalho que demora
time.Sleep(3 * time.Second)
select {
case resultado <- fmt.Sprintf("Dado %d pronto!", id):
// Tudo certo, mandamos o resultado
case <-ctx.Done():
// Opa, o contexto foi cancelado! Melhor abortar.
fmt.Printf("✂️ Goroutine %d foi cancelada\n", id)
}
}
func main() {
resultado := make(chan string)
// Contexto com timeout de 2 segundos (bem curto pra forçar o cancelamento)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
// Lança algumas goroutines
for i := 1; i <= 3; i++ {
go buscarDado(ctx, i, resultado)
}
// Tenta coletar os resultados
for i := 0; i < 3; i++ {
select {
case res := <-resultado:
fmt.Println(res)
case <-ctx.Done():
fmt.Println("⏰ Timeout! Chega de esperar...")
return
}
}
}
Aqui, se as goroutines demorarem mais de 2 segundos, o contexto cancela e a gente para de esperar.
Buffered Channels: Quando Você Quer Mais Flexibilidade 🤸♂️
Até agora usamos channels "normais". Isso significa que a goroutine que envia fica esperando até alguém receber.
Mas às vezes você quer que a goroutine envie e já continue trabalhando. Para isso, usamos buffered channels:
// Channel com buffer para 3 valores
resultado := make(chan string, 3)
// Agora você pode enviar 3 valores sem ficar esperando
resultado <- "valor 1"
resultado <- "valor 2"
resultado <- "valor 3"
// Só vai travar na 4ª tentativa (quando o buffer estiver cheio)
Padrão Real: Agregando Dados de Várias Fontes
Vamos juntar tudo em um exemplo mais próximo da realidade. Imagine buscando preços de criptomoedas:
package main
import (
"context"
"fmt"
"time"
)
type Preco struct {
Moeda string
Valor float64
}
func buscarPreco(ctx context.Context, moeda string, resultado chan Preco) {
// Simula uma chamada de API que demora 1 segundo
time.Sleep(1 * time.Second)
// Preços fictícios para exemplo
precos := map[string]float64{
"BTC": 45000.50,
"ETH": 3200.75,
"LINK": 18.25,
}
select {
case resultado <- Preco{Moeda: moeda, Valor: precos[moeda]}:
fmt.Printf("✅ Preço do %s buscado com sucesso\n", moeda)
case <-ctx.Done():
fmt.Printf("❌ Busca do %s cancelada\n", moeda)
}
}
func main() {
moedas := []string{"BTC", "ETH", "LINK"}
resultado := make(chan Preco, len(moedas))
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Dispara uma goroutine para cada moeda
for _, moeda := range moedas {
go buscarPreco(ctx, moeda, resultado)
}
// Coleta os resultados
fmt.Println("📊 Coletando preços...")
for i := 0; i < len(moedas); i++ {
preco := <-resultado
fmt.Printf("💰 %s: $%.2f\n", preco.Moeda, preco.Valor)
}
fmt.Println("🎉 Todos os preços coletados!")
}
Por Que Go é Tão Bom Nisso?
Go brilha em concorrência porque:
- Goroutines são super leves: você pode ter milhares sem pesar no sistema
- Channels são seguros: sem aqueles bugs malucos de acesso simultâneo
- Sintaxe é simples: não tem callback hell ou Promises complexas
- O runtime cuida de tudo: você só manda e o Go gerencia
Conclusão
Embora concorrência em Go seja algo complexo para quem não está acostumado, ele é um modelo bem pensado onde goroutines trabalham independentemente e channels as mantêm sincronizadas.
Comece simples: uma goroutine, um channel, um resultado. Depois evolua para timeouts, buffered channels e agregação de dados.
O legal é que mesmo em cenários complexos, o código em Go continua legível e direto ao ponto. 💙
Just Code It! 🚀


Top comments (0)