Concorrência em Go: Goroutines e Canais - Guia Completo
Domine a concorrência em Go com goroutines e canais. Padrões avançados, sincronização, instruções select e melhores práticas com exemplos de código detalhados.

A concorrência é uma das maiores forças de Go. Diferente de outras linguagens onde a multithreading permanece complexa, Go oferece um modelo elegante baseado em goroutines e canais que simplifica consideravelmente o desenvolvimento de aplicações concorrentes.
"Não se comunique compartilhando memória; compartilhe memória se comunicando." Este princípio fundamental guia todo o design de concorrência em Go.
Compreendendo as Goroutines
As goroutines são threads leves gerenciadas pelo runtime de Go. Consomem aproximadamente 2 KB de pilha (contra vários MB para threads do sistema operacional) e permitem executar milhares de tarefas concorrentes sem sobrecarga.
Lançar uma goroutine requer simplesmente colocar a palavra-chave go antes de uma chamada de função. O runtime cuida do escalonamento e da distribuição entre as threads disponíveis.
package main
import (
"fmt"
"time"
)
// fetchData simulates a network request
func fetchData(id int) {
// Simulates network delay
time.Sleep(100 * time.Millisecond)
fmt.Printf("Data %d fetched\n", id)
}
func main() {
// Sequential execution - 500ms total
start := time.Now()
for i := 1; i <= 5; i++ {
fetchData(i)
}
fmt.Printf("Sequential: %v\n", time.Since(start))
// Concurrent execution - ~100ms total
start = time.Now()
for i := 1; i <= 5; i++ {
go fetchData(i) // Execute as goroutine
}
time.Sleep(150 * time.Millisecond) // Wait for completion
fmt.Printf("Concurrent: %v\n", time.Since(start))
}A execução concorrente reduz o tempo total de 500ms para aproximadamente 100ms. No entanto, usar time.Sleep para sincronizar goroutines não é uma boa prática. Os canais oferecem uma solução elegante.
Canais: Comunicação Entre Goroutines
Um canal é um conduto tipado para enviar e receber valores entre goroutines. Os canais garantem a sincronização: uma goroutine que envia espera até que outra receba, e vice-versa.
A criação de um canal usa a função make. O operador <- envia e recebe dados conforme sua posição em relação ao canal.
package main
import "fmt"
// worker performs computation and returns result via channel
func worker(id int, jobs <-chan int, results chan<- int) {
// Receives jobs until channel closes
for job := range jobs {
result := job * 2 // Processing
results <- result // Send result
}
}
func main() {
// Create channels
jobs := make(chan int, 10) // Buffered channel
results := make(chan int, 10)
// Start 3 workers
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// Send 5 jobs
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs) // Signal end of jobs
// Collect results
for r := 1; r <= 5; r++ {
result := <-results
fmt.Printf("Result: %d\n", result)
}
}Os canais direcionais (<-chan para recepção, chan<- para envio) reforçam a segurança do código limitando as operações possíveis.
Canais com Buffer vs Sem Buffer
A distinção entre esses tipos de canais afeta diretamente o comportamento de sincronização entre goroutines.
Os canais sem buffer bloqueiam o emissor até que um receptor esteja pronto. Os canais com buffer permitem enviar até N valores sem bloquear, onde N representa a capacidade do buffer.
package main
import "fmt"
func main() {
// Unbuffered channel - strict synchronization
unbuffered := make(chan string)
go func() {
unbuffered <- "message" // Blocks until received
}()
msg := <-unbuffered // Unblocks the send
fmt.Println(msg)
// Buffered channel - capacity of 2
buffered := make(chan int, 2)
// These sends don't block
buffered <- 1
buffered <- 2
// buffered <- 3 // Would block because buffer is full
fmt.Println(<-buffered) // 1
fmt.Println(<-buffered) // 2
// Check capacity
fmt.Printf("Length: %d, Capacity: %d\n",
len(buffered), cap(buffered))
}Os canais com buffer desacoplam produtores e consumidores, enquanto os sem buffer garantem uma sincronização ponto a ponto.
Um deadlock ocorre quando todas as goroutines estão bloqueadas aguardando. O runtime de Go detecta isso e encerra o programa com uma mensagem de erro explícita.
Select: Multiplexação de Canais
A instrução select aguarda operações simultâneas em vários canais. Assemelha-se a um switch para comunicações concorrentes.
Esta construção é essencial para gerenciar timeouts, cancelamentos e múltiplas comunicações sem bloquear indefinidamente em um único canal.
package main
import (
"fmt"
"time"
)
// fetchAPI simulates an API call with variable delay
func fetchAPI(name string, delay time.Duration, ch chan<- string) {
time.Sleep(delay)
ch <- fmt.Sprintf("%s: data received", name)
}
func main() {
api1 := make(chan string)
api2 := make(chan string)
// Launch two API calls in parallel
go fetchAPI("API-1", 100*time.Millisecond, api1)
go fetchAPI("API-2", 200*time.Millisecond, api2)
// Global timeout of 150ms
timeout := time.After(150 * time.Millisecond)
// Collect results with timeout
for i := 0; i < 2; i++ {
select {
case result := <-api1:
fmt.Println(result)
case result := <-api2:
fmt.Println(result)
case <-timeout:
fmt.Println("Timeout - operation cancelled")
return
}
}
}O select escolhe o primeiro canal disponível. Se vários estiverem prontos, a escolha é pseudoaleatória para evitar starvation.
Pronto para mandar bem nas entrevistas de Go?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
Padrão Worker Pool
O padrão worker pool distribui tarefas entre vários workers, limitando a concorrência e otimizando o uso de recursos. Este padrão é indispensável para processar grandes quantidades de dados.
A implementação se baseia em um canal de tarefas compartilhado entre os workers e um canal de resultados para coleta.
package main
import (
"fmt"
"sync"
"time"
)
// Task represents a unit of work
type Task struct {
ID int
Data string
}
// Result contains the processing result
type Result struct {
TaskID int
Output string
}
// worker processes received tasks
func worker(id int, tasks <-chan Task, results chan<- Result, wg *sync.WaitGroup) {
defer wg.Done()
for task := range tasks {
// Simulate processing
time.Sleep(50 * time.Millisecond)
results <- Result{
TaskID: task.ID,
Output: fmt.Sprintf("Worker %d processed: %s", id, task.Data),
}
}
}
func main() {
const numWorkers = 3
const numTasks = 10
tasks := make(chan Task, numTasks)
results := make(chan Result, numTasks)
var wg sync.WaitGroup
// Start workers
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go worker(w, tasks, results, &wg)
}
// Send tasks
for i := 1; i <= numTasks; i++ {
tasks <- Task{ID: i, Data: fmt.Sprintf("task-%d", i)}
}
close(tasks)
// Close results channel after workers finish
go func() {
wg.Wait()
close(results)
}()
// Collect results
for result := range results {
fmt.Printf("Task %d: %s\n", result.TaskID, result.Output)
}
}O sync.WaitGroup coordena a espera por todos os workers antes de fechar o canal de resultados.
Padrão Fan-Out/Fan-In
Este padrão distribui o trabalho entre várias goroutines (fan-out) e depois agrega os resultados (fan-in). Maximiza o paralelismo enquanto simplifica a coleta de resultados.
package main
import (
"fmt"
"sync"
)
// generate produces numbers on a channel
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
// square computes the square of received numbers
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
// merge combines multiple channels into one (fan-in)
func merge(channels ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
// Output function for each channel
output := func(c <-chan int) {
defer wg.Done()
for n := range c {
out <- n
}
}
// Launch a goroutine per channel
wg.Add(len(channels))
for _, c := range channels {
go output(c)
}
// Close after all goroutines finish
go func() {
wg.Wait()
close(out)
}()
return out
}
func main() {
// Generate data
numbers := generate(1, 2, 3, 4, 5, 6, 7, 8)
// Fan-out: distribute to 3 workers
sq1 := square(numbers)
sq2 := square(numbers)
sq3 := square(numbers)
// Fan-in: aggregate results
for result := range merge(sq1, sq2, sq3) {
fmt.Println(result)
}
}Este padrão se destaca para operações CPU-bound distribuíveis e pipelines de processamento de dados.
Context para Cancelamento e Deadlines
O pacote context padroniza o gerenciamento de cancelamentos, deadlines e valores entre goroutines. Qualquer goroutine de longa duração deve aceitar um context como primeiro parâmetro.
package main
import (
"context"
"fmt"
"time"
)
// fetchWithContext simulates a cancellable request
func fetchWithContext(ctx context.Context, url string) (string, error) {
// Simulates a long operation
select {
case <-time.After(2 * time.Second):
return fmt.Sprintf("Data from %s", url), nil
case <-ctx.Done():
return "", ctx.Err() // context.Canceled or context.DeadlineExceeded
}
}
func main() {
// Context with 500ms timeout
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel() // Release resources
result, err := fetchWithContext(ctx, "https://api.example.com")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Println(result)
// Context with manual cancellation
ctx2, cancel2 := context.WithCancel(context.Background())
go func() {
time.Sleep(100 * time.Millisecond)
cancel2() // Explicit cancellation
}()
result, err = fetchWithContext(ctx2, "https://api2.example.com")
if err != nil {
fmt.Printf("Request cancelled: %v\n", err)
}
}Sempre chame defer cancel() imediatamente após criar um context para evitar vazamentos de recursos.
Sincronização com sync.Mutex
Embora os canais sejam preferíveis para comunicação, o pacote sync continua necessário para proteger o acesso concorrente a estruturas de dados compartilhadas.
package main
import (
"fmt"
"sync"
)
// SafeCounter is a thread-safe counter
type SafeCounter struct {
mu sync.Mutex
value map[string]int
}
// Increment increments the value for a given key
func (c *SafeCounter) Increment(key string) {
c.mu.Lock() // Exclusive lock
defer c.mu.Unlock() // Guaranteed unlock
c.value[key]++
}
// Value returns the current value
func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value[key]
}
func main() {
counter := SafeCounter{value: make(map[string]int)}
var wg sync.WaitGroup
// 1000 concurrent increments
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment("visits")
}()
}
wg.Wait()
fmt.Printf("Total: %d\n", counter.Value("visits")) // 1000
}O sync.RWMutex otimiza as leituras concorrentes com RLock()/RUnlock() para operações de somente leitura.
Erros Comuns e Soluções
A concorrência em Go apresenta armadilhas clássicas. Eis os erros mais frequentes e como evitá-los.
package main
import (
"fmt"
"sync"
)
func main() {
// ERROR: Loop variable capture
// All goroutines would print the same value
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // Capture by reference - BUG
}()
}
// SOLUTION: Pass value as parameter
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
fmt.Println(n) // Local copy - CORRECT
}(i)
}
wg.Wait()
// ERROR: Send on nil channel
var ch chan int
// ch <- 1 // Blocks forever
// SOLUTION: Always initialize with make
ch = make(chan int, 1)
ch <- 1
fmt.Println(<-ch)
// ERROR: Send on closed channel
done := make(chan bool)
close(done)
// done <- true // Panic!
// SOLUTION: Check before send or use sync.Once
select {
case done <- true:
fmt.Println("Sent")
default:
fmt.Println("Channel closed or full")
}
}A detecção de race conditions usa a flag -race durante a compilação ou os testes: go test -race ./....
Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
Conclusão
Dominar a concorrência em Go se baseia em alguns conceitos-chave que, quando bem compreendidos, permitem construir aplicações de alto desempenho.
Pontos-chave:
✅ As goroutines são leves e baratas: criar milhares permanece aceitável
✅ Os canais sincronizam e transferem dados entre goroutines
✅ A instrução select gerencia múltiplas comunicações e timeouts
✅ O padrão worker pool limita a concorrência e otimiza recursos
✅ O pacote context padroniza cancelamento e deadlines
✅ Os mutex protegem dados compartilhados quando os canais são insuficientes
✅ A flag -race detecta race conditions durante os testes
A filosofia "Compartilhar memória se comunicando" guia a designs mais seguros e manuteníveis do que o multithreading tradicional com locks.
Tags
Compartilhar
Artigos relacionados

Entrevista Tecnica de Go: Goroutines, Channels e Concorrencia
Perguntas de entrevista tecnica sobre goroutines, channels e padroes de concorrencia em Go. Exemplos de codigo, armadilhas comuns e respostas de nivel avancado para se preparar para entrevistas Go em 2026.

Top 25 perguntas de entrevista Go: guia completo do desenvolvedor
Domine as entrevistas de Go com as 25 perguntas mais frequentes. Goroutines, channels, interfaces e padrões de concorrência com exemplos de código.

Go: Fundamentos para Desenvolvedores Java/Python em 2026
Aprenda Go rapidamente aproveitando sua experiência em Java ou Python. Goroutines, channels, interfaces e padrões essenciais para uma transição tranquila.