A concorrência é uma das features mais poderosas e distintivas de Go. Enquanto outras linguagens tornam a programação paralela complexa e cheia de boilerplate, Go a torna simples e elegante com goroutines e channels.

Neste quarto artigo da série “Golang para Iniciantes”, vamos desvendar os segredos da concorrência em Go. Você aprenderá a executar múltiplas tarefas simultaneamente de forma segura e eficiente.

Se você está chegando agora:

📖 ← Artigo Anterior: Controle de Fluxo em Go

Por Que Concorrência Importa?

O Problema: Execução Sequencial

Imagine buscar dados de 5 APIs diferentes:

// Execução sequencial: lenta!
result1 := buscarAPI1()  // 2 segundos
result2 := buscarAPI2()  // 2 segundos
result3 := buscarAPI3()  // 2 segundos
result4 := buscarAPI4()  // 2 segundos
result5 := buscarAPI5()  // 2 segundos
// Total: 10 segundos

A Solução: Execução Concorrente

// Execução concorrente: rápida!
go buscarAPI1()  // Inicia e continua
igo buscarAPI2()  // Inicia e continua
go buscarAPI3()  // Inicia e continua
go buscarAPI4()  // Inicia e continua
go buscarAPI5()  // Inicia e continua
// Todas executam em paralelo!

Goroutines

Goroutines são funções ou métodos que executam concorrentemente com outras goroutines. São como threads, mas muito mais leves.

Criando Goroutines

Use a palavra-chave go:

package main

import (
    "fmt"
    "time"
)

func dizerOla() {
    for i := 0; i < 3; i++ {
        fmt.Println("Olá!")
        time.Sleep(100 * time.Millisecond)
    }
}

func dizerMundo() {
    for i := 0; i < 3; i++ {
        fmt.Println("Mundo!")
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    go dizerOla()   // Inicia goroutine
    go dizerMundo() // Inicia outra goroutine

    // Aguarda um pouco para as goroutines terminarem
    time.Sleep(1 * time.Second)
    fmt.Println("Fim")
}

Output (a ordem pode variar):

Olá!
Mundo!
Olá!
Mundo!
Olá!
Mundo!
Fim

Goroutines com Funções Anônimas

package main

import (
    "fmt"
    "time"
)

func main() {
    // Goroutine com função anônima
    go func() {
        for i := 0; i < 3; i++ {
            fmt.Println("Goroutine:", i)
            time.Sleep(50 * time.Millisecond)
        }
    }()

    // Código principal continua executando
    for i := 0; i < 3; i++ {
        fmt.Println("Main:", i)
        time.Sleep(50 * time.Millisecond)
    }

    time.Sleep(200 * time.Millisecond)
}

Passando Parâmetros

package main

import (
    "fmt"
    "time"
)

func processar(id int, dados string) {
    fmt.Printf("Goroutine %d: processando %s\n", id, dados)
    time.Sleep(100 * time.Millisecond)
    fmt.Printf("Goroutine %d: concluída\n", id)
}

func main() {
    for i := 1; i <= 5; i++ {
        go processar(i, fmt.Sprintf("tarefa-%d", i))
    }

    time.Sleep(1 * time.Second)
    fmt.Println("Todas as goroutines concluídas")
}

⚠️ Importante: Goroutines são leves (iniciam com ~2KB de stack), mas não são gratuitas. Criar milhões pode exaurir memória.

Channels (Canais)

Channels são a forma de comunicação entre goroutines. Eles permitem que goroutines troquem dados de forma segura.

Criando Channels

package main

func main() {
    // Canal de inteiros sem buffer
    ch := make(chan int)

    // Canal de strings com buffer
    chBuffered := make(chan string, 10)

    _ = ch
    _ = chBuffered
}

Enviando e Recebendo

package main

import "fmt"

func main() {
    mensagens := make(chan string)

    // Goroutine que envia dados
    go func() {
        mensagens <- "Olá do canal!"
    }()

    // Recebe dados do canal
    msg := <-mensagens
    fmt.Println(msg)
}

Nota: A operação de envio (<-) bloqueia até que alguém receba, e a operação de receber (<-) bloqueia até que alguém envie.

Channels com Buffer

Channels bufferizados não bloqueiam imediatamente:

package main

import "fmt"

func main() {
    // Canal com buffer de 2
    mensagens := make(chan string, 2)

    // Envia sem bloquear (ainda há espaço)
    mensagens <- "primeira"
    mensagens <- "segunda"

    // Recebe
    fmt.Println(<-mensagens)
    fmt.Println(<-mensagens)
}

Padrão Worker Pool

Distribui trabalho entre múltiplas goroutines:

package main

import (
    "fmt"
    "time"
)

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Printf("Worker %d: processando job %d\n", id, j)
        time.Sleep(time.Second) // Simula trabalho
        results <- j * 2
    }
}

func main() {
    const numJobs = 10
    const numWorkers = 3

    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)

    // Inicia workers
    for w := 1; w <= numWorkers; w++ {
        go worker(w, jobs, results)
    }

    // Envia jobs
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

    // Coleta resultados
    for a := 1; a <= numJobs; a++ {
        resultado := <-results
        fmt.Printf("Resultado: %d\n", resultado)
    }
}

Sincronização com sync

WaitGroup

Espere que múltiplas goroutines terminem:

package main

import (
    "fmt"
    "sync"
    "time"
)

func processar(id int, wg *sync.WaitGroup) {
    defer wg.Done()  // Decrementa o contador quando terminar

    fmt.Printf("Iniciando tarefa %d\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Tarefa %d concluída\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)           // Incrementa o contador
        go processar(i, &wg)
    }

    wg.Wait()  // Bloqueia até o contador zerar
    fmt.Println("Todas as tarefas concluídas")
}

Mutex

Protege acesso concorrente a dados compartilhados:

package main

import (
    "fmt"
    "sync"
)

type Contador struct {
    mu    sync.Mutex
    valor int
}

func (c *Contador) Incrementar() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.valor++
}

func (c *Contador) Valor() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.valor
}

func main() {
    var wg sync.WaitGroup
    contador := Contador{}

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            contador.Incrementar()
        }()
    }

    wg.Wait()
    fmt.Printf("Contador: %d\n", contador.Valor())
}

RWMutex

Permite múltiplos leitores ou um único escritor:

package main

import (
    "fmt"
    "sync"
    "time"
)

type Cache struct {
    mu    sync.RWMutex
    dados map[string]string
}

func (c *Cache) Get(chave string) (string, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    valor, ok := c.dados[chave]
    return valor, ok
}

func (c *Cache) Set(chave, valor string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.dados[chave] = valor
}

func main() {
    cache := Cache{dados: make(map[string]string)}

    // Escritor
    go func() {
        for i := 0; i < 10; i++ {
            cache.Set(fmt.Sprintf("key%d", i), fmt.Sprintf("value%d", i))
            time.Sleep(100 * time.Millisecond)
        }
    }()

    // Múltiplos leitores
    for i := 0; i < 3; i++ {
        go func(id int) {
            for j := 0; j < 10; j++ {
                if val, ok := cache.Get(fmt.Sprintf("key%d", j)); ok {
                    fmt.Printf("Leitor %d: %s\n", id, val)
                }
                time.Sleep(50 * time.Millisecond)
            }
        }(i)
    }

    time.Sleep(2 * time.Second)
}

Select

O select permite esperar por múltiplas operações de canal simultaneamente.

Select Básico

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "canal 1"
    }()

    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "canal 2"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println("Recebido:", msg1)
        case msg2 := <-ch2:
            fmt.Println("Recebido:", msg2)
        }
    }
}

Select com Timeout

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan string)

    go func() {
        time.Sleep(2 * time.Second)
        ch <- "resultado"
    }()

    select {
    case res := <-ch:
        fmt.Println("Recebido:", res)
    case <-time.After(1 * time.Second):
        fmt.Println("Timeout!")
    }
}

Select Não-Bloqueante

package main

import "fmt"

func main() {
    mensagens := make(chan string)
    sinais := make(chan bool)

    // Envio não-bloqueante
    select {
    case mensagens <- "olá":
        fmt.Println("Mensagem enviada")
    default:
        fmt.Println("Canal cheio, mensagem não enviada")
    }

    // Recebimento não-bloqueante
    select {
    case msg := <-mensagens:
        fmt.Println("Recebido:", msg)
    default:
        fmt.Println("Nenhuma mensagem disponível")
    }

    _ = sinais
}

Padrões de Concorrência

1. Generator (Gerador)

package main

import "fmt"

func geradorNumeros() <-chan int {
    ch := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            ch <- i
        }
        close(ch)
    }()
    return ch
}

func main() {
    for num := range geradorNumeros() {
        fmt.Println(num)
    }
}

2. Pipeline

package main

import "fmt"

// Etapa 1: Gera números
func gerar(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

// Etapa 2: Quadrado
func quadrado(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n
        }
        close(out)
    }()
    return out
}

func main() {
    // Pipeline: gerar -> quadrado
    for resultado := range quadrado(gerar(2, 3, 4, 5)) {
        fmt.Println(resultado) // 4, 9, 16, 25
    }
}

3. Fan-Out / Fan-In

package main

import (
    "fmt"
    "sync"
)

func producer(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

func worker(id int, in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n // Processa
        }
        close(out)
    }()
    return out
}

func merge(chs ...<-chan int) <-chan int {
    var wg sync.WaitGroup
    out := make(chan int)

    output := func(ch <-chan int) {
        for n := range ch {
            out <- n
        }
        wg.Done()
    }

    wg.Add(len(chs))
    for _, ch := range chs {
        go output(ch)
    }

    go func() {
        wg.Wait()
        close(out)
    }()

    return out
}

func main() {
    in := producer(1, 2, 3, 4, 5, 6, 7, 8)

    // Fan-out: múltiplos workers
    c1 := worker(1, in)
    c2 := worker(2, in)
    c3 := worker(3, in)

    // Fan-in: merge dos resultados
    for n := range merge(c1, c2, c3) {
        fmt.Println(n)
    }
}

Exercícios Práticos

Exercício 1: Download Concorrente

Implemente um downloader que baixa múltiplas URLs concorrentemente.

Ver solução
package main

import (
    "fmt"
    "time"
)

func download(url string, resultado chan<- string) {
    // Simula download
    time.Sleep(time.Duration(len(url)) * 100 * time.Millisecond)
    resultado <- fmt.Sprintf("Downloaded: %s", url)
}

func main() {
    urls := []string{
        "https://api1.com",
        "https://api2.com",
        "https://api3.com",
    }

    resultados := make(chan string, len(urls))

    for _, url := range urls {
        go download(url, resultados)
    }

    for i := 0; i < len(urls); i++ {
        fmt.Println(<-resultados)
    }
}

Exercício 2: Soma Concorrente

Calcule a soma de um slice dividindo o trabalho entre goroutines.

Ver solução
package main

import (
    "fmt"
    "sync"
)

func somaParcial(nums []int, resultado chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    soma := 0
    for _, n := range nums {
        soma += n
    }
    resultado <- soma
}

func main() {
    numeros := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    numWorkers := 2

    tamanhoParte := len(numeros) / numWorkers
    resultados := make(chan int, numWorkers)
    var wg sync.WaitGroup

    for i := 0; i < numWorkers; i++ {
        inicio := i * tamanhoParte
        fim := inicio + tamanhoParte
        if i == numWorkers-1 {
            fim = len(numeros)
        }

        wg.Add(1)
        go somaParcial(numeros[inicio:fim], resultados, &wg)
    }

    go func() {
        wg.Wait()
        close(resultados)
    }()

    total := 0
    for parcial := range resultados {
        total += parcial
    }

    fmt.Printf("Soma total: %d\n", total)
}

Dicas e Melhores Práticas

1. Não Use Concorrência Desnecessariamente

// Ruim: overhead de goroutine para tarefa simples
go fmt.Println("Olá")

// Bom: execução sequencial é mais simples
fmt.Println("Olá")

2. Sempre Trate Race Conditions

Use go run -race para detectar condições de corrida:

go run -race meu-programa.go

3. Feche Channels Apenas Quando Apropriado

// Quem envia fecha o canal
func producer(ch chan<- int) {
    defer close(ch)
    for i := 0; i < 10; i++ {
        ch <- i
    }
}

4. Use Context para Cancelamento

package main

import (
    "context"
    "fmt"
    "time"
)

func processar(ctx context.Context, id int) {
    select {
    case <-ctx.Done():
        fmt.Printf("Worker %d cancelado\n", id)
        return
    case <-time.After(time.Second):
        fmt.Printf("Worker %d concluído\n", id)
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    for i := 1; i <= 5; i++ {
        go processar(ctx, i)
    }

    time.Sleep(3 * time.Second)
}

Próximos Passos

Parabéns! Você domina os fundamentos da concorrência em Go. Prepare-se para aplicar esse conhecimento:

Próximo Artigo da Série

📖 Criando sua Primeira API REST em Go → — Construa uma API web completa usando tudo o que aprendeu.

Recursos Recomendados

Desafios

  1. Rate Limiter: Implemente um limitador de requisições usando channels
  2. Crawler Web: Crie um crawler concorrente com limite de profundidade
  3. Chat Simples: Servidor de chat usando channels
  4. Processador de Imagens: Processe imagens em paralelo

Artigo anterior: Controle de Fluxo em Go ←
Próximo artigo: Criando sua Primeira API REST em Go →


Última atualização: 09 de fevereiro de 2026
Versão do Go: 1.23