# 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. - Published: 2026-04-01 - Updated: 2026-04-01 - Author: Anthony Fillion-Maillet - Tags: go, interview, concurrency, goroutines, channels - Reading time: 10 min --- As perguntas de entrevista sobre goroutines, channels e concorrencia em Go estao entre os topicos mais desafiadores que candidatos enfrentam em processos seletivos. Dominar esses conceitos em profundidade e o que diferencia engenheiros Go seniors de desenvolvedores que ainda estao aprendendo a linguagem. Este guia cobre as perguntas exatas que entrevistadores fazem em 2026, com exemplos de codigo prontos para producao e o raciocinio por tras de cada resposta. > **O que entrevistadores realmente avaliam** > > Entrevistas de concorrencia em Go focam em tres areas: gerenciamento do ciclo de vida de goroutines, semantica de channels (buffered vs unbuffered, tipos direcionais) e composicao de padroes (fan-out/fan-in, worker pools, cancelamento com context). Decorar sintaxe nao e suficiente -- entrevistadores esperam que candidatos consigam raciocinar sobre race conditions e deadlocks. ## Fundamentos de Goroutines: O Que Todo Entrevistador Pergunta A primeira rodada de perguntas normalmente investiga se o candidato entende o que goroutines realmente sao -- nao apenas como lanca-las. **P: O que e uma goroutine e como ela difere de uma thread do sistema operacional?** Uma goroutine e uma funcao concorrente leve gerenciada pelo scheduler do runtime de Go, nao pelo sistema operacional. O runtime de Go multiplexa milhares de goroutines em um numero reduzido de threads do SO usando um modelo de escalonamento M:N (M goroutines mapeadas para N threads do SO). ```go // goroutine_basics.go package main import ( "fmt" "runtime" "sync" ) func main() { // Print the number of OS threads available fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0)) var wg sync.WaitGroup for i := 0; i < 10000; i++ { wg.Add(1) go func(id int) { defer wg.Done() // Each goroutine starts with ~2-8KB stack // OS threads typically start with 1-8MB _ = id }(i) } wg.Wait() fmt.Println("All 10,000 goroutines completed") } ``` Diferencas essenciais para mencionar em uma entrevista: goroutines iniciam com uma stack de 2-8KB que cresce dinamicamente, em contraste com a stack fixa de 1-8MB de uma thread do SO. A troca de contexto entre goroutines e gerenciada no espaco do usuario pelo runtime de Go, evitando transicoes custosas entre user space e kernel space. O valor de `GOMAXPROCS` define quantas threads do SO podem executar codigo Go simultaneamente -- por padrao, ele corresponde ao numero de nucleos de CPU disponiveis. **P: O que acontece quando uma goroutine entra em panico? Como evitar que ela derrube o programa inteiro?** Um panico nao recuperado em qualquer goroutine encerra todo o processo. A goroutine principal nao pode capturar panicos de goroutines filhas. A solucao e utilizar `defer` com `recover` dentro de cada goroutine que possa falhar. ```go // panic_recovery.go package main import "fmt" func safeGo(fn func()) { go func() { defer func() { if r := recover(); r != nil { fmt.Println("recovered from panic:", r) } }() fn() // execute the actual work }() } func main() { safeGo(func() { panic("something went wrong") }) // Give goroutine time to complete select {} } ``` Em producao, essa wrapper `safeGo` normalmente e combinada com logging estruturado para registrar panicos recuperados. Esse padrao aparece frequentemente em servidores HTTP, onde cada requisicao e tratada em uma goroutine separada e uma falha nao deve derrubar o servidor inteiro. ## Semantica de Channels: Buffered, Unbuffered e Direcionalidade Channels sao o mecanismo principal de comunicacao entre goroutines em Go. Entrevistadores avaliam se o candidato compreende as diferencas fundamentais entre channels buffered e unbuffered, e quando usar cada um. **P: Qual a diferenca entre um channel buffered e um unbuffered?** Um channel unbuffered (`make(chan T)`) bloqueia o sender ate que um receiver esteja pronto, criando um ponto de sincronizacao. Um channel buffered (`make(chan T, N)`) permite ao sender enviar ate N valores sem bloquear, desacoplando temporalmente sender e receiver. ```go // channel_semantics.go package main import "fmt" func main() { // Unbuffered: send blocks until receive is ready ch := make(chan string) go func() { ch <- "hello" // blocks here until main reads }() msg := <-ch fmt.Println(msg) // Buffered: send does not block until buffer is full buf := make(chan int, 3) buf <- 1 // does not block (buffer has space) buf <- 2 // does not block buf <- 3 // does not block // buf <- 4 would block — buffer is full fmt.Println(<-buf, <-buf, <-buf) // 1 2 3 } ``` A regra pratica: channels unbuffered sao usados quando o sender precisa de confirmacao de que o receiver processou o valor. Channels buffered sao usados para desacoplar produtores e consumidores que operam em velocidades diferentes. **P: Como sinalizar para um consumidor que nao ha mais valores a serem enviados?** O padrao idiomatico e fechar o channel com `close(ch)`. Isso permite que o consumidor use `range` para iterar ate que o channel seja fechado. Apenas o produtor deve fechar o channel -- nunca o consumidor. ```go // close_channel.go package main import "fmt" func producer(ch chan<- int, count int) { for i := 0; i < count; i++ { ch <- i } close(ch) // signal: no more values } func main() { ch := make(chan int, 5) go producer(ch, 5) // range exits automatically when channel closes for val := range ch { fmt.Println("received:", val) } // Reading from closed channel returns zero value + false val, ok := <-ch fmt.Printf("after close: val=%d, ok=%v\n", val, ok) } ``` Um detalhe que demonstra senioridade na entrevista: enviar para um channel fechado causa panico. Por isso, a responsabilidade de fechar sempre pertence ao lado que envia. O idioma com dois valores de retorno (`val, ok := <-ch`) permite verificar se o channel foi fechado sem arriscar um panico. ## Instrucao Select: Multiplexacao de Channels O `select` e uma construcao central na concorrencia de Go que permite esperar simultaneamente em multiplas operacoes de channel. Entrevistadores frequentemente pedem que candidatos implementem timeouts e cancelamentos usando `select`. **P: Como implementar um timeout para uma chamada de API usando select e context?** O `select` bloqueia ate que uma das suas operacoes de channel esteja pronta. Quando combinado com `context.WithTimeout`, ele oferece controle preciso sobre cancelamento e deadlines. ```go // select_multiplex.go package main import ( "context" "fmt" "time" ) func fetchFromAPI(ctx context.Context, url string) (string, error) { resultCh := make(chan string, 1) errCh := make(chan error, 1) go func() { // Simulate API call time.Sleep(200 * time.Millisecond) resultCh <- fmt.Sprintf("data from %s", url) }() select { case result := <-resultCh: return result, nil case err := <-errCh: return "", err case <-ctx.Done(): // Context cancelled or timed out return "", ctx.Err() } } func main() { ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() result, err := fetchFromAPI(ctx, "https://api.example.com/data") if err != nil { fmt.Println("error:", err) return } fmt.Println(result) } ``` Quando multiplos cases estao prontos simultaneamente, o `select` escolhe um aleatoriamente -- esse comportamento evita starvation. O caso `default` torna o select nao-bloqueante, util para polling, mas deve ser usado com cautela para evitar busy-waiting. ## Padroes de Concorrencia: Fan-Out/Fan-In e Worker Pools Padroes de concorrencia representam a categoria mais avancada de perguntas em entrevistas Go. Dois padroes aparecem com frequencia consistente: fan-out/fan-in e worker pools. ### Fan-Out/Fan-In: Paralelismo com Channels Fan-out distribui trabalho entre multiplas goroutines que leem do mesmo channel de entrada. Fan-in coleta os resultados em um unico channel de saida. ```go // fanout_fanin.go package main import ( "fmt" "sync" ) // generator produces values on a channel func generator(nums ...int) <-chan int { out := make(chan int) go func() { for _, n := range nums { out <- n } close(out) }() return out } // square reads from input, squares each value func square(in <-chan int) <-chan int { out := make(chan int) go func() { for n := range in { out <- n * n } close(out) }() return out } // fanIn merges multiple channels into one func fanIn(channels ...<-chan int) <-chan int { var wg sync.WaitGroup merged := make(chan int) for _, ch := range channels { wg.Add(1) go func(c <-chan int) { defer wg.Done() for val := range c { merged <- val } }(ch) } go func() { wg.Wait() close(merged) // close after all inputs are drained }() return merged } func main() { in := generator(2, 3, 4, 5, 6) // Fan out: two goroutines reading from same channel c1 := square(in) c2 := square(in) // Fan in: merge results for result := range fanIn(c1, c2) { fmt.Println(result) } } ``` O padrao fan-out/fan-in e ideal quando o processamento de cada item e independente e pode ser paralelizado. O `WaitGroup` na funcao `fanIn` garante que o channel de saida so e fechado apos todos os channels de entrada serem drenados. ### Worker Pool com errgroup: Concorrencia Limitada Em cenarios de producao, limitar a concorrencia e essencial para evitar sobrecarga de recursos. O pacote `golang.org/x/sync/errgroup` oferece uma solucao elegante com propagacao automatica de erros. ```go // worker_pool.go package main import ( "context" "fmt" "golang.org/x/sync/errgroup" ) func processItem(ctx context.Context, id int) error { // Check for cancellation before heavy work select { case <-ctx.Done(): return ctx.Err() default: } if id == 7 { return fmt.Errorf("failed to process item %d", id) } fmt.Printf("processed item %d\n", id) return nil } func main() { g, ctx := errgroup.WithContext(context.Background()) g.SetLimit(3) // maximum 3 concurrent goroutines for i := 0; i < 10; i++ { id := i g.Go(func() error { return processItem(ctx, id) }) } // Wait blocks until all goroutines finish // Returns the first non-nil error if err := g.Wait(); err != nil { fmt.Println("pipeline error:", err) } } ``` O `errgroup.WithContext` cria um context derivado que e cancelado automaticamente quando qualquer goroutine retorna um erro. O metodo `SetLimit(3)` restringe a execucao a no maximo 3 goroutines simultaneas, funcionando como um semaforo. ## Race Conditions: Detectando e Prevenindo Corridas de Dados Perguntas sobre race conditions testam se o candidato compreende os riscos do acesso concorrente a memoria compartilhada e conhece as ferramentas que Go oferece para evita-los. **P: O que e uma race condition e quais mecanismos Go oferece para evita-la?** Uma race condition ocorre quando duas ou mais goroutines acessam a mesma variavel simultaneamente e pelo menos uma delas realiza uma escrita. O resultado e nao-deterministico. Go oferece tres mecanismos de protecao: operacoes atomicas (`sync/atomic`), mutexes (`sync.Mutex`/`sync.RWMutex`) e channels. ```go // race_condition.go package main import ( "fmt" "sync" "sync/atomic" ) // BAD: race condition \u2014 do not use in production func unsafeCounter() int { counter := 0 var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() counter++ // DATA RACE: concurrent read/write }() } wg.Wait() return counter // result is non-deterministic } // GOOD: atomic operations for simple counters func atomicCounter() int64 { var counter atomic.Int64 var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() counter.Add(1) // thread-safe atomic increment }() } wg.Wait() return counter.Load() // always 1000 } // GOOD: mutex for complex shared state type SafeMap struct { mu sync.RWMutex data map[string]int } func (m *SafeMap) Set(key string, val int) { m.mu.Lock() // exclusive lock for writes defer m.mu.Unlock() m.data[key] = val } func (m *SafeMap) Get(key string) (int, bool) { m.mu.RLock() // shared lock for reads defer m.mu.RUnlock() v, ok := m.data[key] return v, ok } func main() { fmt.Println("unsafe:", unsafeCounter()) // unpredictable fmt.Println("atomic:", atomicCounter()) // always 1000 } ``` A ferramenta `go test -race` (race detector) deve ser executada em todo pipeline de CI. Ela instrumenta o binario em tempo de compilacao e detecta acessos concorrentes a memoria compartilhada em tempo de execucao. Usar `sync.RWMutex` em vez de `sync.Mutex` permite multiplas leituras simultaneas, melhorando a performance em cenarios com alta proporcao de leituras. ## Context e Cancelamento: Controlando o Ciclo de Vida de Goroutines O pacote `context` e fundamental para gerenciar timeouts, deadlines e cancelamento em aplicacoes Go concorrentes. Entrevistadores esperam que candidatos saibam propagar contexts corretamente. **P: Como o context controla o cancelamento de goroutines em cascata?** Um context pai cancelado automaticamente cancela todos os contexts filhos. Isso permite encerrar arvores inteiras de goroutines com uma unica chamada a `cancel()`, evitando goroutine leaks. ```go // context_cancellation.go package main import ( "context" "fmt" "time" ) // worker simulates a long-running task func worker(ctx context.Context, id int, results chan<- string) { select { case <-time.After(time.Duration(id*100) * time.Millisecond): results <- fmt.Sprintf("worker %d: done", id) case <-ctx.Done(): results <- fmt.Sprintf("worker %d: cancelled (%v)", id, ctx.Err()) } } func main() { // Parent context with 250ms deadline ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond) defer cancel() results := make(chan string, 5) // Launch 5 workers with increasing durations for i := 1; i <= 5; i++ { go worker(ctx, i, results) } // Collect all results for i := 0; i < 5; i++ { fmt.Println(<-results) } } ``` Neste exemplo, workers com duracao menor que 250ms completam normalmente, enquanto os demais recebem o sinal de cancelamento. Isso demonstra o padrao de cancelamento cooperativo -- goroutines precisam verificar ativamente `ctx.Done()` para responder ao cancelamento. > **Nunca armazene context em structs** > > A documentacao oficial de Go e explicita: `context.Context` deve ser passado como primeiro parametro de funcoes, nunca armazenado em campos de structs. Armazenar contexts em structs torna impossivel controlar o ciclo de vida corretamente, pois contexts derivados (com timeout ou cancelamento) nao podem ser substituidos de forma segura em campos compartilhados. ## Deteccao de Deadlocks: Quando Goroutines Ficam Presas Deadlocks sao um dos erros mais comuns em codigo concorrente. O runtime de Go detecta automaticamente quando todas as goroutines estao bloqueadas, mas nao consegue detectar deadlocks parciais. **P: O que causa um deadlock em Go e como identifica-lo?** Um deadlock ocorre quando goroutines esperam indefinidamente por recursos que nunca serao liberados. O caso mais simples e enviar para um channel unbuffered sem um receiver correspondente. ```go // deadlock_example.go package main func main() { ch := make(chan int) ch <- 42 // DEADLOCK: unbuffered send with no receiver // The main goroutine blocks here forever // Go runtime detects this: "fatal error: all goroutines are asleep" } ``` > **Deadlocks parciais nao sao detectados pelo runtime** > > O runtime de Go so detecta deadlocks quando **todas** as goroutines estao bloqueadas. Se pelo menos uma goroutine permanece ativa (por exemplo, um HTTP server escutando), deadlocks parciais passam despercebidos. Para diagnosticar esses casos, utilizar `pprof` com o endpoint `/debug/pprof/goroutine` revela goroutines bloqueadas e suas stacks. Causas comuns de deadlock em entrevistas: enviar e receber no mesmo channel na mesma goroutine, esquecer de fechar channels usados com `range`, e locks adquiridos em ordem inconsistente entre goroutines. ## Processamento com Rate Limiting: Semaforos e Tickers Rate limiting e um topico avancado que combina multiplos primitivos de concorrencia. Entrevistadores usam essa pergunta para avaliar a capacidade do candidato de compor padroes. **P: Como implementar um rate limiter que controla tanto a concorrencia quanto a taxa de processamento?** A combinacao de um channel como semaforo (limitando concorrencia) com um `time.Ticker` (limitando taxa temporal) oferece controle preciso. O `select` com context permite cancelamento gracioso. ```go // rate_limited.go package main import ( "context" "fmt" "sync" "time" ) // RateLimiter controls concurrent and temporal access type RateLimiter struct { semaphore chan struct{} // limits concurrency ticker *time.Ticker // limits rate } func NewRateLimiter(maxConcurrent int, interval time.Duration) *RateLimiter { return &RateLimiter{ semaphore: make(chan struct{}, maxConcurrent), ticker: time.NewTicker(interval), } } func (rl *RateLimiter) Execute(ctx context.Context, fn func() error) error { // Wait for rate limit tick select { case <-rl.ticker.C: case <-ctx.Done(): return ctx.Err() } // Acquire concurrency slot select { case rl.semaphore <- struct{}{}: case <-ctx.Done(): return ctx.Err() } defer func() { <-rl.semaphore }() // release slot return fn() } func main() { rl := NewRateLimiter(3, 100*time.Millisecond) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func(id int) { defer wg.Done() err := rl.Execute(ctx, func() error { fmt.Printf("[%v] processing %d\n", time.Now().Format("04:05.000"), id) time.Sleep(150 * time.Millisecond) // simulate work return nil }) if err != nil { fmt.Printf("item %d: %v\n", id, err) } }(i) } wg.Wait() } ``` Esse padrao aparece em clientes HTTP, processadores de filas e integracao com APIs externas que impoe limites de taxa. O channel buffered (`semaphore`) atua como semaforo de contagem, enquanto o ticker garante espacamento temporal entre execucoes. ## Conclusao - Goroutines sao threads de espaco de usuario gerenciadas pelo runtime de Go com escalonamento M:N; sempre recuperar panicos em goroutines lancadas - Channels unbuffered sincronizam sender e receiver; channels buffered desacoplam o tempo -- escolher com base na necessidade de confirmacao pelo sender - A instrucao `select` multiplexa operacoes de channel com selecao aleatoria quando multiplas estao prontas; combinar com `context.Context` para timeouts - Fan-out/fan-in e worker pools (via `errgroup.SetLimit`) sao os dois padroes de concorrencia mais frequentemente perguntados - Usar `sync.Mutex` para estado compartilhado complexo, `sync/atomic` para contadores simples e channels para comunicacao entre goroutines - Sempre executar `go test -race` no CI para detectar data races; deadlocks parciais requerem `pprof` para diagnostico - Nunca armazenar `context.Context` em structs -- passa-lo como primeiro parametro de funcao - Rate limiting em Go combina semaforos com channels e tickers, encapsulados em instrucoes select com suporte a context --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pt/blog/go/go-technical-interview-goroutines-channels-concurrency