M01·03Concorrência: O Superpoder do Go

CAPÍTULO 03

Concorrência: O Superpoder do Go

Goroutines, channels, select e context — como Go torna trivial o que é complexo em outras linguagens.

Por Thiago Souza12 min de leituraAtualizado em 2026-05

Aqui está a razão pela qual Go conquistou o mundo backend. Concorrência em Go é tão simples que parece mágica.

O problema que estamos resolvendo

Imagine que seu servidor precisa atender 10.000 pessoas simultaneamente. Em linguagens tradicionais com threads do sistema operacional, criar 10.000 threads consome gigabytes de RAM e o sistema engasga.

Go resolve isso com goroutines — threads "leves" gerenciadas pelo próprio Go. Você pode ter milhões delas rodando e o sistema nem sente.

Goroutines: rodando coisas em paralelo

Para rodar uma função concorrentemente, basta colocar go antes dela. Sério, é isso.

go
package main
 
import (
    "fmt"
    "time"
)
 
func greet(name string) {
    for i := 0; i < 3; i++ {
        fmt.Println("Olá,", name)
        time.Sleep(100 * time.Millisecond)
    }
}
 
func main() {
    // Roda normalmente (sequencialmente)
    greet("Ana")
 
    // Roda em paralelo!
    go greet("Bia")
    go greet("Caio")
 
    // Precisamos esperar, senão o main termina antes das goroutines.
    time.Sleep(1 * time.Second)
    fmt.Println("Fim do programa")
}
Goroutine é como pedir um Uber e continuar conversando com seus amigos. O Uber está sendo chamado em paralelo, você não fica parado esperando.

Pegadinha clássica

go
// ERRADO! main termina antes das goroutines rodarem.
func main() {
    go fmt.Println("oi") // pode nem imprimir!
}

Por isso precisamos de mecanismos de sincronização. Entram os channels.

Channels: comunicação entre goroutines

Channels são o jeito idiomático de fazer goroutines conversarem entre si. Pense em canos: você joga dados de um lado, alguém pega do outro.

Don't communicate by sharing memory; share memory by communicating. Em vez de várias threads disputando uma variável compartilhada (com locks), uma manda dados via channel para a outra. Mais simples e mais seguro.

go
package main
 
import "fmt"
 
func main() {
    // Cria um channel que transporta inteiros
    ch := make(chan int)
 
    // Goroutine que envia dados
    go func() {
        ch <- 42 // ENVIAR para o channel
    }()
 
    // Recebe do channel (bloqueia até alguém enviar)
    valor := <-ch
    fmt.Println("Recebi:", valor)
}

A seta <- indica direção:

  • ch <- 42enviar 42 para o channel
  • valor := <-chreceber do channel

Exemplo realista: processando trabalhos em paralelo

go
package main
 
import (
    "fmt"
    "time"
)
 
func trabalhador(id int, trabalhos <-chan int, resultados chan<- int) {
    // <-chan = só pode RECEBER
    // chan<- = só pode ENVIAR
    for trabalho := range trabalhos {
        fmt.Printf("Trabalhador %d processando %d\n", id, trabalho)
        time.Sleep(time.Second) // simula trabalho pesado
        resultados <- trabalho * 2
    }
}
 
func main() {
    trabalhos := make(chan int, 5)
    resultados := make(chan int, 5)
 
    // Cria 3 trabalhadores
    for i := 1; i <= 3; i++ {
        go trabalhador(i, trabalhos, resultados)
    }
 
    // Envia 5 trabalhos
    for j := 1; j <= 5; j++ {
        trabalhos <- j
    }
    close(trabalhos) // sinaliza que não há mais trabalhos
 
    // Coleta resultados
    for k := 1; k <= 5; k++ {
        fmt.Println("Resultado:", <-resultados)
    }
}
O make(chan int, 5) cria um channel com buffer de 5. Envios não bloqueiam até encher o buffer. Sem buffer, cada envio bloqueia até alguém receber.

Select: o "switch" para channels

O select permite esperar vários channels ao mesmo tempo. O primeiro que tiver algo, ganha.

go
package main
 
import (
    "fmt"
    "time"
)
 
func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
 
    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "vem do ch1"
    }()
 
    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "vem do ch2"
    }()
 
    // Espera o que vier primeiro
    for i := 0; i < 2; i++ {
        select {
        case msg := <-ch1:
            fmt.Println(msg)
        case msg := <-ch2:
            fmt.Println(msg)
        case <-time.After(3 * time.Second):
            fmt.Println("timeout!")
            return
        }
    }
}
select é como um garçom que está vigiando várias mesas ao mesmo tempo. A primeira que chamar, ele atende.

Context: cancelamento e timeout

context é fundamental em backends. Ele permite cancelar operações que demoram demais ou foram abortadas pelo usuário.

Imagine um endpoint HTTP. O cliente desistiu e fechou o navegador. Por que seu servidor continuaria processando aquela requisição? context resolve isso.

go
package main
 
import (
    "context"
    "fmt"
    "time"
)
 
func operacaoLenta(ctx context.Context) error {
    select {
    case <-time.After(5 * time.Second):
        // operação demorada que terminou
        fmt.Println("operação concluída")
        return nil
    case <-ctx.Done():
        // o context foi cancelado!
        return ctx.Err()
    }
}
 
func main() {
    // Context com timeout de 2 segundos
    ctx, cancelar := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancelar() // SEMPRE chame cancel para liberar recursos
 
    err := operacaoLenta(ctx)
    if err != nil {
        fmt.Println("Erro:", err) // "context deadline exceeded"
    }
}
Regra de ouro do backend: toda função que faz I/O (banco, HTTP, rede) deve receber um context.Context como primeiro parâmetro. Esse é um padrão universal em Go. Idiomático: func BuscarUsuario(ctx context.Context, id int) (*Usuario, error). Sem context: foge do padrão e perde cancelamento, timeout e tracing.