Resposta rápida: em Go (Golang), goroutines executam funções concorrentemente com go f() e channels são canais tipados criados com make(chan T). Envie com ch <- valor e receba com valor := <-ch. Sem buffer, a comunicação exige remetente e receptor prontos; com buffer, o envio pode prosseguir enquanto há espaço. Use close(ch) para sinalizar que não haverá novos valores e for v := range ch para consumir até o canal ser fechado e esvaziado (especificação oficial).
Neste quarto artigo da série Golang para Iniciantes, você pratica goroutines, go channels, select, WaitGroup e mutex com exemplos que compilam. Se quiser o guia mais amplo de produção (leaks, worker pool, errgroup), use também Concorrência em Go no Aprenda.
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
go buscarAPI2() // Inicia e continua
go buscarAPI3() // Inicia e continua
go buscarAPI4() // Inicia e continua
go buscarAPI5() // Inicia e continua
// As chamadas foram iniciadas concorrentemente.
Esses trechos são esquemáticos: as funções buscarAPI* precisam ser implementadas, os resultados coletados e o programa deve aguardar as goroutines. Iniciar com go não garante paralelismo nem ganho de velocidade; isso depende do trabalho e dos recursos disponíveis. Quando main retorna, o programa termina sem esperar as outras goroutines (especificação).
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")
}
Goroutine x Thread do Sistema
Muita gente chega aqui pesquisando goroutines vindo de outras linguagens, comparando com threads tradicionais. A diferença está no custo e em quem faz o escalonamento (FAQ oficial do Go):
| Aspecto | Goroutine | Thread do sistema |
|---|---|---|
| Quem escalona | Runtime do Go (espaço de usuário) | Kernel do sistema operacional |
| Stack | Começa com poucos KB e pode crescer | Tamanho e configuração dependem da plataforma |
| Quantidade | O FAQ considera prático criar centenas de milhares | Limites dependem do sistema e dos recursos |
| Comunicação típica | channels | Memória compartilhada / IPC |
O runtime multiplexa goroutines sobre threads do sistema. GOMAXPROCS limita quantas threads podem executar código Go simultaneamente, não o número total de goroutines ou threads (documentação de runtime).
⚠️ Importante: Goroutines não são gratuitas. Limite a concorrência de acordo com memória, conexões e capacidade dos serviços acessados; não existe um custo universal de criação ou troca de contexto para toda plataforma.
Go Channels (Canais)
Channels em Go são canais tipados para comunicação entre goroutines. Em vez de compartilhar memória com locks o tempo todo, você envia e recebe valores pelo canal — o slogan clássico do Go: do not communicate by sharing memory; share memory by communicating (Effective Go).
| Precisa de… | Prefira |
|---|---|
| Iniciar trabalho concorrente | goroutine (go f()) |
| Passar resultado, erro ou sinal entre tarefas | go channel (make(chan T)) |
| Só esperar um grupo terminar, sem payload | sync.WaitGroup |
| Timeout, cancelamento ou vários canais | select + context |
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: Neste exemplo o canal não tem buffer: o envio e o recebimento precisam se encontrar. Em canais com buffer, o envio bloqueia quando não há espaço e o recebimento bloqueia quando não há valores, salvo quando o canal está fechado e vazio (especificação).
Exemplo completo: goroutine, close e range
Salve como main.go e rode go run -race main.go:
package main
import "fmt"
func main() {
numeros := make(chan int)
go func() {
defer close(numeros) // Este produtor é o único responsável pelos envios.
for n := 1; n <= 3; n++ {
numeros <- n
}
}()
for n := range numeros {
fmt.Println(n)
}
fmt.Println("Canal fechado e consumido")
}
A saída é 1, 2, 3 e Canal fechado e consumido, uma linha por valor. O range recebe os valores na ordem de envio e termina depois do fechamento e do consumo de todos os valores. Sem close, este laço continuaria esperando; neste programa, isso causaria deadlock. Não é necessário usar time.Sleep para esperar: o recebimento coordena o consumidor com o produtor (regra de range sobre canais).
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)
}
Channel Nil, Aberto e Fechado
As regras abaixo vêm direto da especificação da linguagem e são as mais cobradas em entrevista — vale memorizar esta tabela:
| Operação | Channel nil | Channel aberto | Channel fechado |
|---|---|---|---|
Envio ch <- v | bloqueia para sempre | bloqueia até alguém receber (ou haver buffer) | panic |
Recebimento <-ch | bloqueia para sempre | bloqueia até haver valor | consome o buffer com ok=true; quando vazio, retorna valor zero e ok=false |
close(ch) | panic | fecha com sucesso | panic |
package main
import "fmt"
func main() {
ch := make(chan int, 1)
ch <- 42
close(ch)
v, ok := <-ch // primeira leitura após close: entrega o valor 42, ok=true
fmt.Println(v, ok)
v, ok = <-ch // buffer vazio e canal fechado: zero, ok=false
fmt.Println(v, ok)
// ch <- 1 // panic: send on closed channel
}
Regra prática: quem coordena os envios é responsável pelo fechamento. Com vários produtores, espere todos terminarem antes de chamar close, como no exemplo de soma concorrente abaixo. Fechar não descarta os valores do buffer, e canais não precisam ser fechados para liberar memória: o fechamento sinaliza o fim dos envios (Go Blog: pipelines).
Canais Direcionais (chan<- e <-chan)
No worker pool abaixo, as assinaturas jobs <-chan int e results chan<- int usam canais direcionais, que restringem o uso:
func produtor(ch chan<- int) // só pode ENVIAR
func consumidor(ch <-chan int) // só pode RECEBER
// um channel bidirecional é convertido na atribuição:
ch := make(chan int) // chan int (bidirecional)
var soEnvia chan<- int = ch
var soRecebe <-chan int = ch
Usar canais direcionais em assinaturas transforma erros de uso em erros de compilação — o compilador impede, por exemplo, que um worker envie na fila de jobs.
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, 1) // Espaço para o único resultado mesmo após o timeout.
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!")
}
}
O timeout para de esperar, mas não cancela a goroutine. Aqui o buffer de tamanho 1 permite que o único envio termine caso o consumidor desista. Em uma operação real de I/O, propague context.Context para cancelar o trabalho; para vários envios, cada produtor precisa observar o cancelamento. O Go Blog explica esse problema em Stopping short.
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
}
Desativando um Case com Channel nil
A tabela de ciclo de vida acima mostra que enviar ou receber de um channel nil bloqueia para sempre — e isso é útil. Em um select, um case cujo canal é nil nunca fica pronto, então atribuir nil a um canal “desliga” aquele case sem precisar de flags extras. É a forma clássica de consumir vários canais até todos fecharem, usada inclusive no padrão fan-in do Go Blog:
package main
import (
"fmt"
"time"
)
func main() {
numeros := make(chan int)
go func() {
defer close(numeros)
for i := 1; i <= 3; i++ {
numeros <- i
}
}()
leitor := numeros // chan int não-nulo: case ativo
for leitor != nil {
select {
case v, ok := <-leitor:
if !ok {
leitor = nil // desativa o case: <-nil nunca fica pronto
continue
}
fmt.Println("valor:", v)
case <-time.After(500 * time.Millisecond):
fmt.Println("timeout")
leitor = nil
}
}
fmt.Println("fim")
}
Saída: valor: 1, valor: 2, valor: 3, fim. Quando numeros fecha e esvazia, ok é false; atribuir nil a leitor faz o select ignorar esse case e o laço termina pela condição leitor != nil. Em código de produção, prefira context.WithTimeout em vez de time.After dentro de laços: cada time.After cria um timer novo a cada iteração (documentação de time).
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)
}
}
4. Ticker — um channel que emite tempo
time.Ticker é um channel de tempo: ticker.C recebe um time.Time a cada intervalo. Combina naturalmente com select e cancelamento:
package main
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(200 * time.Millisecond)
defer ticker.Stop() // interrompe o ticker; sem Stop o timer só para no fim do programa
encerrar := time.After(700 * time.Millisecond)
for {
select {
case t := <-ticker.C:
fmt.Println("tick em", t.Format("15:04:05.000"))
case <-encerrar:
fmt.Println("fim")
return
}
}
}
Saída: três linhas tick em … (em ~200 ms, ~400 ms e ~600 ms) e depois fim. Sempre chame ticker.Stop() para liberar o timer; o atalho time.Tick só é adequado quando o ticker vive até o fim do programa, porque não pode ser interrompido (documentação de Ticker).
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 de goroutines e go channels. O próximo passo é aplicar isso em I/O de rede e, depois, aprofundar os padrões de produção.
Próximo Artigo da Série
📖 Criando sua Primeira API REST em Go → — Construa uma API web completa usando tudo o que aprendeu.
Continuar no site
- 📘 Concorrência em Go (guia Aprenda) — modelo mental, leaks e checklist de produção
- 📬 Channels em produção — ownership, close e backpressure
- 🧵 Worker pool em Go — fila de jobs com limite de concorrência
- 🔒 sync.Mutex, WaitGroup e RWMutex — quando o estado compartilhado é inevitável
- 🧪 Testes em Go com -race — caçar condições de corrida cedo
Recursos oficiais
- 📚 Go Concurrency Patterns
- 📖 Especificação: tipos de channel
- 📬 Go Blog: pipelines e cancelamento
- 🧪 Documentação do race detector
- 📖 Effective Go: Concurrency
Desafios
- Rate Limiter: Implemente um limitador de requisições usando channels
- Crawler Web: Crie um crawler concorrente com limite de profundidade
- Chat Simples: Servidor de chat usando channels
- 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: 8 de outubro de 2026
Versão do Go: 1.27.1