DEV Community

Cover image for Concorrência em Go: Goroutines e Channels Easy
Adriano P. Araujo
Adriano P. Araujo

Posted on

Concorrência em Go: Goroutines e Channels Easy

Antes de Começar: Concorrência vs Paralelismo vs Assíncrono

Qual a diferênça?

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')
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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 😴
Enter fullscreen mode Exit fullscreen mode

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! 🚀
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

É 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
}
Enter fullscreen mode Exit fullscreen mode

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!")
}
Enter fullscreen mode Exit fullscreen mode

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,
    }
}
Enter fullscreen mode Exit fullscreen mode

O que está rolando aqui?

  1. Criamos um channel chamado pricesChan
  2. Lançamos várias goroutines, cada uma busca um token diferente
  3. Cada goroutine manda seu resultado pelo channel
  4. 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

esperando

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
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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!")
}
Enter fullscreen mode Exit fullscreen mode

Por Que Go é Tão Bom Nisso?

Go brilha em concorrência porque:

  1. Goroutines são super leves: você pode ter milhares sem pesar no sistema
  2. Channels são seguros: sem aqueles bugs malucos de acesso simultâneo
  3. Sintaxe é simples: não tem callback hell ou Promises complexas
  4. 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)