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.
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")
}Pegadinha clássica
// 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.
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 <- 42→ enviar 42 para o channelvalor := <-ch→ receber do channel
Exemplo realista: processando trabalhos em paralelo
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)
}
}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.
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.
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"
}
}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.