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.

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.
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).
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.
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.
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.
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.
Pronto para mandar bem nas entrevistas de Go?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
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.
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.
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.
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.
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.
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.
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.
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"
}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.
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.
Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
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
selectmultiplexa operacoes de channel com selecao aleatoria quando multiplas estao prontas; combinar comcontext.Contextpara timeouts - Fan-out/fan-in e worker pools (via
errgroup.SetLimit) sao os dois padroes de concorrencia mais frequentemente perguntados - Usar
sync.Mutexpara estado compartilhado complexo,sync/atomicpara contadores simples e channels para comunicacao entre goroutines - Sempre executar
go test -raceno CI para detectar data races; deadlocks parciais requerempprofpara diagnostico - Nunca armazenar
context.Contextem 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
Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
Tags
Compartilhar
Artigos relacionados

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.

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.