Janeiro 2026 · ~16 min

Concorrência em Go: Goroutines e Channels - Guia Completo

Aprenda go channels e goroutines em Go: go func(), make(chan T), select, WaitGroup e go test -race. Exemplos práticos da série Golang para Iniciantes.

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):

AspectoGoroutineThread do sistema
Quem escalonaRuntime do Go (espaço de usuário)Kernel do sistema operacional
StackComeça com poucos KB e pode crescerTamanho e configuração dependem da plataforma
QuantidadeO FAQ considera prático criar centenas de milharesLimites dependem do sistema e dos recursos
Comunicação típicachannelsMemó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 concorrentegoroutine (go f())
Passar resultado, erro ou sinal entre tarefasgo channel (make(chan T))
Só esperar um grupo terminar, sem payloadsync.WaitGroup
Timeout, cancelamento ou vários canaisselect + 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çãoChannel nilChannel abertoChannel fechado
Envio ch <- vbloqueia para semprebloqueia até alguém receber (ou haver buffer)panic
Recebimento <-chbloqueia para semprebloqueia até haver valorconsome o buffer com ok=true; quando vazio, retorna valor zero e ok=false
close(ch)panicfecha com sucessopanic
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

Recursos oficiais

Desafios

  1. Rate Limiter: Implemente um limitador de requisições usando channels
  2. Crawler Web: Crie um crawler concorrente com limite de profundidade
  3. Chat Simples: Servidor de chat usando channels
  4. 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

Perguntas frequentes

O que são goroutines em Go?

Goroutines executam funções concorrentemente, gerenciadas pelo runtime do Go. Você cria uma com go minhaFunc() ou go func() { ... }(). Suas stacks começam com poucos kilobytes e crescem conforme necessário. Channels permitem comunicar valores entre goroutines, mas dados compartilhados ainda precisam de sincronização.

O que são go channels e para que servem?

Channels (canais) são o meio tipado de comunicação entre goroutines. Com ch := make(chan T) você envia com ch <- valor e recebe com v := <-ch. Channels sem buffer sincronizam remetente e destinatário; com buffer, o envio só bloqueia quando o buffer enche. A busca por channels golang ou go channels aponta para o mesmo recurso da linguagem.

Go routines e goroutines são a mesma coisa?

Sim. Go routines (escrito separado) é a forma comum de buscar o conceito; o nome oficial da linguagem é goroutine, uma só palavra. Em código você sempre usa a palavra-chave go antes da chamada: go f().

Qual a diferença entre concorrência e paralelismo em Go?

Concorrência é estruturar o programa para lidar com várias tarefas ao mesmo tempo (goroutines + channels). Paralelismo é executar essas tarefas ao mesmo tempo em mais de um núcleo. Em Go você escreve código concorrente; o runtime pode paralelizar quando GOMAXPROCS e o hardware permitem.

Quando usar WaitGroup em vez de channel?

Use sync.WaitGroup quando só precisa esperar um conjunto de goroutines terminar. Use channel quando precisa passar resultados, erros ou sinais entre elas. Em APIs reais, combine WaitGroup ou errgroup com context.Context para cancelamento.

Qual a diferença entre goroutine e thread?

Goroutines são gerenciadas pelo runtime do Go e multiplexadas sobre threads do sistema operacional. Suas stacks começam com poucos kilobytes e podem crescer; o FAQ oficial considera prático criar centenas de milhares. Threads são gerenciadas pelo sistema operacional, e seus custos e tamanhos de stack dependem da plataforma.

O que acontece ao enviar ou receber de um channel fechado?

Enviar para um channel fechado causa panic. Receber dele entrega primeiro os valores que ainda estão no buffer, com ok=true; depois retorna imediatamente o valor zero do tipo e ok=false. Enviar ou receber de um channel nil bloqueia para sempre; close de um channel nil ou já fechado causa panic. Quem coordena os envios deve fechá-lo apenas quando não houver mais envios.

Como detectar race conditions em Go?

Rode go run -race ou go test -race. O race detector aponta acessos concorrentes sem sincronização a variáveis compartilhadas. Corrija com channels, mutex ou atomic — e garanta que toda goroutine tenha um caminho de saída para evitar leaks.