O que é concorrência em Go (e o que ela não garante)
Concorrência é a capacidade de estruturar um programa para lidar com múltiplas tarefas ao mesmo tempo (ou intercaladas), sem assumir uma ordem fixa de execução. Em Go, isso é feito principalmente com goroutines e channels.
Pontos essenciais para evitar bugs:
- Goroutines não garantem ordem: a linha que você escreveu primeiro não necessariamente executa primeiro.
- Não assuma timing: usar
time.Sleeppara “esperar” goroutines é frágil. - Concorrência não é paralelismo: paralelismo depende de múltiplos núcleos e do runtime; concorrência é como você organiza o trabalho.
Agendamento (scheduler) e por que a ordem muda
O runtime de Go possui um agendador que distribui goroutines entre threads do sistema operacional. Ele pode pausar e retomar goroutines em pontos diferentes, então o interleaving muda de execução para execução. Por isso, sincronização deve ser feita com primitivas apropriadas (channels, WaitGroup, Mutex), e não por suposição.
Criando goroutines na prática
Uma goroutine é iniciada com a palavra-chave go antes de uma chamada de função. Ela executa concorrentemente com a goroutine principal.
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
fmt.Println("tarefa A")
}()
go func() {
defer wg.Done()
fmt.Println("tarefa B")
}()
wg.Wait()
fmt.Println("fim")
}
Passo a passo:
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
- Crie um
sync.WaitGrouppara esperar tarefas. - Chame
wg.Add(n)antes de iniciar as goroutines. - Em cada goroutine, use
defer wg.Done(). - No final,
wg.Wait()bloqueia até todas terminarem.
Armadilha comum: captura de variável em loop
Ao iniciar goroutines dentro de um for, cuidado com a variável do loop. Se você capturar a variável diretamente, todas as goroutines podem ver o mesmo valor (o valor final do loop), dependendo do timing.
// ERRADO (pode imprimir valores repetidos/inesperados)
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i)
}()
}
Corrija passando o valor como parâmetro:
// CERTO
for i := 0; i < 5; i++ {
i := i
go func(v int) {
fmt.Println(v)
}(i)
}
Channels: comunicação e sincronização
Channels permitem que goroutines se comuniquem com segurança, transferindo valores entre elas. Um channel também pode ser usado como mecanismo de sincronização (quem envia pode bloquear até alguém receber, e vice-versa).
Channel não bufferizado (unbuffered)
Um channel não bufferizado sincroniza remetente e destinatário: o envio bloqueia até haver um recebimento.
package main
import "fmt"
func main() {
ch := make(chan string) // unbuffered
go func() {
ch <- "olá" // bloqueia até alguém receber
}()
msg := <-ch
fmt.Println(msg)
}
Passo a passo:
- Crie o channel com
make(chan T). - Envie com
ch <- valor. - Receba com
valor := <-ch.
Channel bufferizado (buffered)
Um channel bufferizado tem uma fila interna com capacidade. Envios só bloqueiam quando o buffer está cheio; recebimentos bloqueiam quando está vazio.
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
// ch <- 4 // bloquearia (buffer cheio)
fmt.Println(<-ch) // 1
Use buffer quando fizer sentido desacoplar produtor e consumidor, mas evite “chutar” tamanhos sem medir: buffer não substitui um desenho correto de fluxo.
Fechamento de channel e range
Fechar um channel sinaliza que não haverá mais envios. Receber de um channel fechado retorna o valor zero do tipo e um booleano indicando se ainda havia valores.
v, ok := <-ch
if !ok {
// channel fechado e drenado
}
O padrão idiomático para consumir até o fim é for range:
for v := range ch {
fmt.Println(v)
}
Regra importante: em geral, quem envia é quem deve fechar o channel. Fechar do lado do consumidor costuma causar pânico (envio em channel fechado) se ainda houver produtores ativos.
Sincronização com WaitGroup e proteção com Mutex
WaitGroup para esperar um conjunto de goroutines
sync.WaitGroup é ideal quando você precisa aguardar um conjunto de tarefas terminar, sem necessariamente trocar dados por channel.
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
// trabalho
}()
wg.Wait()
Mutex para proteger estado compartilhado
Se múltiplas goroutines acessam e modificam a mesma variável (por exemplo, um mapa), você precisa proteger esse acesso. Em Go, mapas não são seguros para escrita concorrente.
package main
import (
"sync"
)
func main() {
counts := map[string]int{}
var mu sync.Mutex
var wg sync.WaitGroup
words := []string{"go", "go", "chan", "go"}
wg.Add(len(words))
for _, w := range words {
w := w
go func() {
defer wg.Done()
mu.Lock()
counts[w]++
mu.Unlock()
}()
}
wg.Wait()
_ = counts
}
Prefira channels para coordenar fluxo e Mutex para proteger estado compartilhado. Muitas vezes, um desenho com channels evita a necessidade de compartilhar estado.
Padrões de concorrência
Worker pool (pool de trabalhadores)
Um worker pool limita a quantidade de goroutines trabalhando ao mesmo tempo. Isso é útil para evitar abrir milhares de goroutines e saturar CPU, disco ou rede.
Ideia: um channel de jobs alimenta N workers; um channel de resultados coleta saídas.
package main
import (
"fmt"
"sync"
)
type Job struct {
ID int
}
type Result struct {
JobID int
Out string
}
func worker(id int, jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) {
defer wg.Done()
for j := range jobs {
results <- Result{JobID: j.ID, Out: fmt.Sprintf("worker %d processou job %d", id, j.ID)}
}
}
func main() {
jobs := make(chan Job)
results := make(chan Result)
var wg sync.WaitGroup
workers := 3
wg.Add(workers)
for i := 1; i <= workers; i++ {
go worker(i, jobs, results, &wg)
}
go func() {
for j := 1; j <= 10; j++ {
jobs <- Job{ID: j}
}
close(jobs)
}()
go func() {
wg.Wait()
close(results)
}()
for r := range results {
fmt.Println(r.Out)
}
}
Passo a passo:
- Crie
jobseresults. - Inicie N workers lendo de
jobsaté o channel fechar. - Feche
jobsquando terminar de produzir. - Feche
resultsquando todos os workers terminarem (viaWaitGroup). - Consuma
resultscomrange.
Fan-out / Fan-in
Fan-out: distribuir trabalho para várias goroutines. Fan-in: juntar resultados em um único fluxo.
Um jeito comum de fazer fan-in é ter um channel de resultados compartilhado e fechar esse channel quando todos os produtores terminarem (novamente com WaitGroup).
func fanIn[T any](chs ...<-chan T) <-chan T {
out := make(chan T)
var wg sync.WaitGroup
wg.Add(len(chs))
for _, ch := range chs {
ch := ch
go func() {
defer wg.Done()
for v := range ch {
out <- v
}
}()
}
go func() {
wg.Wait()
close(out)
}()
return out
}
select para timeouts e cancelamento
select permite esperar por múltiplas operações de channel. Ele é a base para timeouts, cancelamento e multiplexação de eventos.
Timeout com time.After
select {
case v := <-ch:
_ = v
case <-time.After(500 * time.Millisecond):
// timeout
}
Cancelamento com context.Context
Para cancelamento cooperativo, use context. A goroutine deve checar ctx.Done() e encerrar quando cancelado.
func doWork(ctx context.Context, in <-chan int, out chan<- int) {
for {
select {
case <-ctx.Done():
return
case v, ok := <-in:
if !ok {
return
}
out <- v * 2
}
}
}
Dica: prefira context.WithCancel ou context.WithTimeout para controlar o ciclo de vida de pipelines e pools.
Exercício completo: processar múltiplos arquivos em paralelo com segurança
Objetivo: dado um conjunto de caminhos de arquivos, processá-los em paralelo com um limite de workers, calcular estatísticas (linhas e bytes) e retornar um relatório. O exercício deve ser seguro (sem data races), lidar com erros e suportar cancelamento/timeout.
Especificação
- Entrada: lista de caminhos (
[]string). - Saída: mapa
map[string]FileStatcom estatísticas por arquivo. - Concorrência: usar worker pool com N workers.
- Sincronização: usar channels para jobs/resultados e
WaitGrouppara fechamento correto. - Segurança: agregar resultados em uma única goroutine (evita Mutex) ou usar Mutex se preferir atualizar o mapa de várias goroutines.
- Cancelamento: se ocorrer erro, cancelar o contexto para parar o restante.
Implementação (passo a passo)
1) Defina tipos para job, resultado e estatísticas.
type FileJob struct {
Path string
}
type FileStat struct {
Bytes int64
Lines int64
}
type FileResult struct {
Path string
Stat FileStat
Err error
}
2) Crie uma função que processa um arquivo. Ela deve ser determinística e não compartilhar estado global.
func computeFileStat(path string) (FileStat, error) {
f, err := os.Open(path)
if err != nil {
return FileStat{}, err
}
defer f.Close()
var st FileStat
buf := make([]byte, 32*1024)
for {
n, rerr := f.Read(buf)
if n > 0 {
st.Bytes += int64(n)
for _, b := range buf[:n] {
if b == '\n' {
st.Lines++
}
}
}
if rerr == io.EOF {
break
}
if rerr != nil {
return FileStat{}, rerr
}
}
return st, nil
}
3) Implemente o worker: ele recebe jobs, processa e envia resultados. Ele deve respeitar cancelamento via context.
func fileWorker(ctx context.Context, jobs <-chan FileJob, results chan<- FileResult, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case j, ok := <-jobs:
if !ok {
return
}
st, err := computeFileStat(j.Path)
select {
case <-ctx.Done():
return
case results <- FileResult{Path: j.Path, Stat: st, Err: err}:
}
}
}
}
4) Orquestre o pool, feche channels corretamente e agregue resultados com segurança em uma única goroutine consumidora.
func ProcessFiles(ctx context.Context, paths []string, workers int) (map[string]FileStat, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
jobs := make(chan FileJob)
results := make(chan FileResult)
var wg sync.WaitGroup
wg.Add(workers)
for i := 0; i < workers; i++ {
go fileWorker(ctx, jobs, results, &wg)
}
go func() {
defer close(jobs)
for _, p := range paths {
select {
case <-ctx.Done():
return
case jobs <- FileJob{Path: p}:
}
}
}()
go func() {
wg.Wait()
close(results)
}()
stats := make(map[string]FileStat, len(paths))
for r := range results {
if r.Err != nil {
cancel()
return nil, fmt.Errorf("processando %s: %w", r.Path, r.Err)
}
stats[r.Path] = r.Stat
}
return stats, nil
}
O que este desenho garante:
- Não há escrita concorrente no mapa: apenas a goroutine agregadora (a que faz
range results) escreve emstats. jobsé fechado pelo produtor (goroutine que envia os jobs).resultsé fechado somente após todos os workers terminarem (wg.Wait()).- Em caso de erro,
cancel()sinaliza para workers e produtor pararem o quanto antes.
Variação: agregando com Mutex (quando fizer sentido)
Se você quiser que cada worker atualize diretamente o mapa compartilhado, use sync.Mutex. Isso pode ser útil quando você não quer um channel de resultados, mas exige cuidado para não misturar controle de fluxo com estado compartilhado.
func ProcessFilesWithMutex(ctx context.Context, paths []string, workers int) (map[string]FileStat, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
jobs := make(chan string)
stats := make(map[string]FileStat, len(paths))
var mu sync.Mutex
var firstErr error
var errMu sync.Mutex
var wg sync.WaitGroup
wg.Add(workers)
for i := 0; i < workers; i++ {
go func() {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case p, ok := <-jobs:
if !ok {
return
}
st, err := computeFileStat(p)
if err != nil {
errMu.Lock()
if firstErr == nil {
firstErr = fmt.Errorf("processando %s: %w", p, err)
cancel()
}
errMu.Unlock()
return
}
mu.Lock()
stats[p] = st
mu.Unlock()
}
}
}()
}
go func() {
defer close(jobs)
for _, p := range paths {
select {
case <-ctx.Done():
return
case jobs <- p:
}
}
}()
wg.Wait()
if firstErr != nil {
return nil, firstErr
}
return stats, nil
}
Checklist de depuração (para evitar suposições)
- Existe algum
time.Sleepusado como sincronização? Substitua porWaitGroupou channels. - Algum mapa está sendo escrito por mais de uma goroutine? Use agregador único ou
Mutex. - Quem fecha cada channel? Garanta que o produtor fecha e que não há envios após o fechamento.
- Se há cancelamento, as goroutines checam
ctx.Done()em pontos de bloqueio? - Se há fan-in, o channel de saída é fechado somente quando todos os produtores terminam?