Uma entrevista de Go sénior não é um teste de sintaxe. Ninguém que esteja a contratar um engenheiro sénior quer saber se consegues recitar o ciclo for de três cláusulas ou se te lembras de que append pode realocar memória. Querem saber se consegues encontrar uma fuga de goroutines, defender a forma como tratas os erros e desenhar uma API com que a equipa consiga viver durante dois anos. As perguntas são abertas de propósito. O entrevistador está a observar a forma como pensas, não a verificar se decoraste a biblioteca padrão.
Este guia cobre os temas que distinguem os candidatos sénior dos de nível intermédio, com código executável para cada um. Baseia-se no que os programadores de Go no ativo dizem construir e nas dificuldades que dizem ter, e não em listas de «50 perguntas mais frequentes» recolhidas da internet.
Resumo rápido
- A concorrência é onde a preparação compensa mais. As duas coisas que os programadores de Go mais constroem são serviços de API/RPC (75 %) e ferramentas de linha de comandos (62 %), e ambas dependem muito de goroutines, channels e
context(Go Developer Survey 2024 H2). - O maior desafio reportado é escrever código idiomático, não a falta de funcionalidades. 33 % dos inquiridos apontaram «garantir que o nosso código Go segue as boas práticas e os idiomas» como o seu maior desafio, mais do que os que apontaram qualquer funcionalidade em falta na linguagem (2025 Go Developer Survey).
- As data races acontecem em produção, por isso conta com elas nas entrevistas. O race detector da Uber encontrou cerca de 2.000 data races no monorepo de Go da empresa, e 210 engenheiros corrigiram cerca de 1.100 delas ao longo de seis meses (Uber Engineering).
- Domina
errors.Is,errors.Ase%wde olhos fechados. 28 % dos inquiridos disseram que falta ao Go uma funcionalidade que valorizavam noutra linguagem, e o tratamento de erros está no topo dessa lista, por isso os entrevistadores investigam como lidas com ele (2025 Go Developer Survey). - Os generics são matéria de entrevista, mas também são uma armadilha. Chegaram no Go 1.18, que a equipa do Go descreveu como «a maior mudança de sempre na linguagem» (Go Blog). O sinal de senioridade é saber quando não os usar.
- Usa a toolchain atual como referência. O Go 1.26 é a versão estável mais recente (notas de versão do Go 1.26). Os entrevistadores reparam quando descreves o comportamento de uma versão antiga.
Índice
- O que uma entrevista de Go sénior avalia realmente
- Concorrência: o núcleo de qualquer entrevista de Go sénior
- Context: cancelamento e prazos
- Tratamento de erros à maneira sénior
- Generics: saber quando recorrer a eles
- Interfaces e design de APIs
- A ronda de live coding: constrói um rate limiter
- Perguntas frequentes
- Começa o teu percurso para Go sénior
O que uma entrevista de Go sénior avalia realmente
Não existe nenhum inquérito de referência que classifique «as perguntas mais comuns em entrevistas de Go», e deves desconfiar de qualquer blog que apresente uma percentagem exata. O que podes fazer é partir daquilo em que os programadores de Go passam o tempo. O inquérito de 2024 concluiu que 75 % dos inquiridos constroem serviços de API ou RPC e 62 % constroem ferramentas de linha de comandos (Go Developer Survey 2024 H2). Esse trabalho é concorrente, depende da rede e tem muitas formas de falhar, e é por isso que as entrevistas voltam sempre à concorrência, a context e ao tratamento de erros.
O inquérito de 2025 perguntou quais eram os maiores desafios. 33 % apontaram seguir as boas práticas e os idiomas. 28 % apontaram uma funcionalidade que valorizavam noutra linguagem e que o Go não tem, sobretudo o tratamento de erros, os sum types e a segurança contra ponteiros nil. 26 % apontaram encontrar módulos de confiança (2025 Go Developer Survey). Boa parte de uma entrevista sénior é a empresa a verificar se interiorizaste os idiomas que um terço da comunidade acha difíceis.
Por isso, a entrevista é sobretudo um teste de discernimento. Escolhes a primitiva de concorrência certa ou lanças uma goroutine por reflexo? Consegues explicar porque encapsulaste um erro em vez de o registares em log e seguires em frente? Consegues dizer «aqui eu não usaria generics» e justificar?
Concorrência: o núcleo de qualquer entrevista de Go sénior
A concorrência é onde os candidatos de nível intermédio perdem ofertas. O erro clássico é lançar uma goroutine por cada unidade de trabalho, sem limite, sem caminho para os erros e sem forma de a parar. Os candidatos sénior partem de um padrão com limite.
As goroutines são baratas, e é isso que torna a abordagem ingénua tentadora. Da FAQ do Go:
A stack inicial tem cerca de 2KB na maioria das plataformas e é gerida pelo runtime, e é por isso que um programa consegue correr centenas de milhares de goroutines ao mesmo tempo. Mas barato não quer dizer gratuito. Goroutines sem limite consomem memória, inundam os serviços a jusante e escondem fugas. A solução habitual é um worker pool.
Worker pools
Um worker pool lança um número fixo de goroutines e dá-lhes trabalho através de um channel. É a resposta habitual a «processa 10.000 URLs sem abrir 10.000 sockets ao mesmo tempo».
example.gogopackage main import ( "fmt" "sync" ) // fetchStatus stands in for any bounded I/O call (HTTP, DB, RPC). func fetchStatus(url string) string { return "200 " + url } func main() { urls := []string{"a", "b", "c", "d", "e"} const workers = 3 jobs := make(chan string) results := make(chan string) var wg sync.WaitGroup for range workers { wg.Go(func() { for url := range jobs { results <- fetchStatus(url) } }) } // Close results once every worker has returned. go func() { wg.Wait() close(results) }() go func() { for _, url := range urls { jobs <- url } close(jobs) }() for r := range results { fmt.Println(r) } }
Os entrevistadores estão atentos a três detalhes. Fechas jobs para que os ciclos range dos workers terminem. Usas um sync.WaitGroup para saber quando é seguro fechar results. E fechas results a partir de uma goroutine separada para que o ciclo principal o consiga esvaziar. Acerta nestes pontos e mostras que percebes o conjunto. Há dois detalhes mais recentes que mostram que acompanhas a toolchain. for range workers itera sobre um inteiro, o que funciona desde o Go 1.22. wg.Go, adicionado no Go 1.25, substitui o antigo par wg.Add(1) mais defer wg.Done(), e isso elimina o bug mais comum com WaitGroup, que é esquecer uma das metades do par.
Data races e o race detector
Conta com uma pergunta do tipo «o que está errado neste código» com uma data race lá dentro. As races aparecem constantemente em código de produção, não só em entrevistas. A Uber publicou os números. O monorepo de Go da empresa tem cerca de 50 milhões de linhas de código distribuídas por cerca de 2.100 serviços Go distintos. O race detector encontrou perto de 2.000 data races, e 210 engenheiros corrigiram cerca de 1.100 delas em seis meses (Uber Engineering).
A data race de manual é um contador partilhado:
example.gogo// BROKEN: concurrent writes to count are a data race. func countBroken(items []int) int { count := 0 var wg sync.WaitGroup for _, n := range items { wg.Add(1) go func() { defer wg.Done() if n%2 == 0 { count++ // unsynchronized write } }() } wg.Wait() return count }
A correção certa depende da forma do problema. Um contador simples pede sync/atomic ou um sync.Mutex. Na agregação, muitas vezes funciona melhor um channel, em que cada goroutine comunica o seu resultado e uma única goroutine é dona do total. Uma resposta sénior identifica o compromisso. Um mutex é mais fácil de ler, as operações atómicas são mais rápidas quando há contenção e um channel («partilhar memória através da comunicação») elimina por completo o estado partilhado. Depois diz «corria isto com go test -race», porque é esse hábito que apanha as races antes de chegarem a produção.
Fan-out, fan-in
O fan-out distribui o trabalho por várias goroutines e o fan-in junta os resultados num único channel. Usa-se para I/O paralelo que alimenta um único consumidor, e combina naturalmente com o worker pool acima. Os candidatos costumam atrapalhar-se no passo de junção. Fechar o channel combinado exige um WaitGroup sobre os produtores, tal como o channel results do worker pool. Se conseguires explicar porque é que os dois padrões fecham a saída da mesma forma, mostras que percebes a posse dos channels e que não estás só a repetir um snippet.
Context: cancelamento e prazos
Os serviços Go passam context.Context ao longo de toda a stack de chamadas, e os entrevistadores vão pedir-te que o uses corretamente. As regras são que context é o primeiro parâmetro, chama-se ctx e nunca o guardas numa struct. Transporta cancelamento, prazos e valores com âmbito de pedido através das fronteiras das APIs.
A pergunta de seguimento mais comum é «como paras um worker que está bloqueado numa chamada lenta?». Fazes um select sobre o trabalho e sobre ctx.Done():
example.gogofunc process(ctx context.Context, jobs <-chan string) error { for { select { case <-ctx.Done(): return ctx.Err() // context.Canceled or DeadlineExceeded case job, ok := <-jobs: if !ok { return nil // channel closed, work done } if err := handle(ctx, job); err != nil { return fmt.Errorf("handling %q: %w", job, err) } } } }
Devolver ctx.Err() diz a quem chamou porque paraste, para que consiga distinguir um prazo expirado de um cancelamento deliberado. As respostas mais fracas ignoram o context ou só o verificam no topo do ciclo. Nesse caso, uma goroutine presa dentro de handle nunca dá pelo cancelamento. Os candidatos fortes também referem que o código que cria o context tem de chamar a função cancel devolvida por context.WithCancel ou context.WithTimeout, normalmente com defer cancel(), para libertar recursos.
Prepara-te para «qual é a diferença entre context.WithCancel e context.WithTimeout?». O primeiro cancela quando chamas cancel. O segundo também cancela quando passa um prazo. Deves fazer defer cancel() em ambos os casos, porque deixar escapar a goroutine interna de um context também é uma fuga de recursos.
Tratamento de erros à maneira sénior
O tratamento de erros é a funcionalidade de outras linguagens de que os programadores de Go mais sentem falta. Em 2025, 28 % dos inquiridos disseram que faltava ao Go uma funcionalidade que valorizavam noutra linguagem, e o tratamento de erros liderava essa lista (2025 Go Developer Survey). É por causa dessa frustração que os entrevistadores o avaliam. Querem ver que tratas os erros como valores com significado, e não como ruído que registas em log e esqueces.
O mínimo esperado é encapsular com %w para que quem chama possa inspecionar a cadeia:
example.gogovar ErrNotFound = errors.New("not found") func loadUser(ctx context.Context, id string) (*User, error) { row, err := db.Query(ctx, id) if err != nil { // Wrap, don't replace. The caller can still see the root cause. return nil, fmt.Errorf("loadUser %s: %w", id, err) } if row == nil { return nil, fmt.Errorf("loadUser %s: %w", id, ErrNotFound) } return row, nil }
Depois, errors.Is procura um valor sentinela e errors.As desembrulha até um tipo concreto:
example.gogouser, err := loadUser(ctx, id) if errors.Is(err, ErrNotFound) { http.Error(w, "user not found", http.StatusNotFound) return } var validationErr *ValidationError if errors.As(err, &validationErr) { http.Error(w, validationErr.Field+" is invalid", http.StatusBadRequest) return }
Prepara-te para explicar quando usar cada um. Um sentinela com errors.Is serve quando quem chama só precisa de saber qual foi o erro. Um erro tipado com errors.As serve quando quem chama precisa de dados dele, como o nome de um campo ou um código de estado. Encapsula com %w apenas quando o erro encapsulado faz mesmo parte do contrato da tua API, porque encapsular em excesso expõe detalhes de implementação. Diz também em voz alta que panic serve para bugs do programador e para falhas irrecuperáveis no arranque, não para condições esperadas como um registo em falta. Usar panic para um erro normal é uma forma rápida de chumbar numa triagem sénior.
Generics: saber quando recorrer a eles
Os generics chegaram no Go 1.18. Foi assim que a equipa do Go os anunciou:
Os entrevistadores perguntam por generics para perceber se te sabes conter. A resposta honesta é que a maior parte do código não precisa deles, e os pacotes slices e maps da biblioteca padrão já cobrem os casos comuns. Usa parâmetros de tipo quando, de outra forma, copiarias a mesma lógica para vários tipos, ou recorrerias a interface{} e perderias a segurança de tipos.
Um bom uso é uma função auxiliar com restrições que a biblioteca padrão ainda não oferece:
example.gogoimport "cmp" // Clamp constrains v to the range [lo, hi] for any ordered type. func Clamp[T cmp.Ordered](v, lo, hi T) T { if v < lo { return lo } if v > hi { return hi } return v }
A restrição cmp.Ordered do pacote padrão cmp limita T a tipos que suportam < e >. A escolha do exemplo também diz algo sobre ti. Não escrevas um Max genérico. O Go tem max e min built-in desde o 1.21, e slices.Max e slices.Min tratam dos slices, por isso uma versão feita à mão sugere que recorres a generics por hábito. Clamp justifica o parâmetro de tipo porque nada na biblioteca padrão faz o mesmo. Antes dos generics, uma função destas exigia uma cópia para cada tipo ou interface{} com type assertions que podiam provocar um panic em runtime. Agora é o compilador que a verifica. Mas, se a função só alguma vez lidar com int, escreve-a para int. A generalização prematura é um code smell em Go como em qualquer outro lado, e o entrevistador espera que o digas.
Interfaces e design de APIs
As perguntas sobre interfaces avaliam o gosto no design, que é uma grande parte do que «sénior» significa. O provérbio de Go a conhecer aqui é «aceita interfaces, devolve structs». Uma função deve aceitar a interface mais estreita que realmente usa e devolver um tipo concreto, para que quem a chama mantenha as opções em aberto.
example.gogo// Good: accepts the minimal behavior it needs. func Copy(dst io.Writer, src io.Reader) (int64, error) { return io.Copy(dst, src) }
A Copy não quer saber se src é um ficheiro, uma ligação de rede ou um buffer. Como aceita io.Reader, funciona com todos eles. É por isso que io.Reader e io.Writer, com um método cada, são as interfaces mais reutilizadas da linguagem.
Há mais alguns pontos que vale a pena referir. Mantém as interfaces pequenas, idealmente com um ou dois métodos. Define-as no pacote que as consome e não no que as implementa, para que os pacotes não dependam uns dos outros sem razão. E larga o hábito, comum noutras linguagens, de pôr uma interface à frente de tudo. Uma struct com uma única implementação ainda não precisa de interface. Acrescenta uma quando uma segunda implementação ou um test double precisarem mesmo dela. Os entrevistadores reparam em abstrações criadas por especulação, e é com esse tipo de código que lidam os 33 % que têm dificuldade com os idiomas (2025 Go Developer Survey).
A ronda de live coding: constrói um rate limiter
A ronda ao vivo costuma pedir algo pequeno mas concorrente. Os rate limiters aparecem muito porque combinam goroutines, channels, tempo e limpeza de recursos. Um limitador do tipo token bucket feito com time.Ticker é curto o suficiente para escrever na sala e dá bastante assunto para conversa:
example.gogotype Limiter struct { tokens chan struct{} stop chan struct{} } func NewLimiter(perSecond int) *Limiter { l := &Limiter{ tokens: make(chan struct{}, perSecond), // burst capacity stop: make(chan struct{}), } ticker := time.NewTicker(time.Second / time.Duration(perSecond)) go func() { defer ticker.Stop() for { select { case <-ticker.C: select { case l.tokens <- struct{}{}: // refill one token default: // bucket full, drop the tick } case <-l.stop: return } } }() return l } // Allow reports whether a request may proceed right now. func (l *Limiter) Allow() bool { select { case <-l.tokens: return true default: return false } } func (l *Limiter) Close() { close(l.stop) }
O código importa menos do que a forma como falas sobre ele. Mostra que o channel tokens com buffer te dá capacidade de burst sem esforço extra. Explica que o select interior com um default descarta os ticks de reposição quando o bucket está cheio, em vez de bloquear. Refere que Close termina a goroutine em segundo plano para que não fique a vazar, e que ticker.Stop() liberta o temporizador. Depois fala da alternativa. Em produção, normalmente usarias golang.org/x/time/rate em vez de escreveres isto tu mesmo, e saber quando escolher a solução padrão em vez de uma feita por medida faz parte do discernimento que estão a contratar.
Perguntas frequentes
Que temas surgem mais numa entrevista de Go sénior?
Concorrência, context, tratamento de erros, interfaces e generics, mais ou menos por esta ordem. A ponderação acompanha o que os programadores de Go constroem. 75 % escrevem serviços de API/RPC e 62 % escrevem ferramentas de CLI (Go Developer Survey 2024 H2), e os dois tipos de trabalho são concorrentes e têm muitas formas de falhar. Não existe nenhuma estatística publicada e credível sobre a frequência com que surgem perguntas específicas, por isso desconfia de qualquer blog que afirme ter uma.
Preciso de saber generics para uma entrevista de Go?
Deves percebê-los, e importa ainda mais que saibas quando não os usar. Os generics chegaram no Go 1.18 como a maior mudança de sempre na linguagem (Go Blog), por isso são matéria de entrevista. Ainda assim, a maior parte do Go idiomático continua a evitá-los, e os candidatos sénior ganham pontos ao explicar que os pacotes padrão slices e maps cobrem a maioria das necessidades.
Qual é a importância do race detector?
É importante ao ponto de os entrevistadores esperarem que o menciones sem ninguém te perguntar. O race detector da Uber encontrou perto de 2.000 data races no monorepo de Go da empresa, e 210 engenheiros corrigiram cerca de 1.100 delas ao longo de seis meses (Uber Engineering). Dizer «corria go test -race» mostra ao entrevistador que já trabalhaste em código de produção.
Que versão do Go devo ter como alvo?
A toolchain estável atual, que no início de 2026 é o Go 1.26 (notas de versão do Go 1.26). Descrever o comportamento de versões antigas mostra que estás desatualizado, por exemplo afirmar que precisas de um ciclo de três cláusulas para iterar sobre um inteiro. A forma for range n funciona desde o Go 1.22.
Ainda vale a pena especializar-me em Go?
Sim. 17,4 % dos programadores profissionais disseram ter trabalhado extensivamente com Go no último ano (Stack Overflow Developer Survey 2025). No inquérito oficial, 91 % dos participantes disseram estar satisfeitos com a linguagem, e 63 % disseram estar muito satisfeitos (2025 Go Developer Survey). A procura por engenheiros de Go sénior acompanha essa adoção.
Começa o teu percurso para Go sénior
Ler sobre estes temas não quer dizer que os consigas escrever sob pressão. As entrevistas sénior recompensam a memória muscular: um worker pool que escreves sem pensar, uma cadeia de erros que encapsulas por reflexo, um generic que sabes deixar de fora. Chegas lá a escrever Go a sério todos os dias com a toolchain atual, com feedback sobre se o teu código é idiomático. Esse feedback é a parte difícil de encontrar, e os idiomas são exatamente aquilo com que 33 % da comunidade Go tem mais dificuldade.
O LevelUpGo foi feito para isso. Cada lição é um exercício no navegador que corre com a versão estável mais recente do Go e avalia o teu código automaticamente, para praticares da forma como a entrevista te avalia em vez de só leres.
Onde praticar cada tema deste artigo:
- Concorrência,
contexte channels: o curso Go Fundamentals desenvolve goroutines, channels e cancelamento a partir dos princípios básicos. É grátis para começar e não precisas de cartão de crédito. - Tratamento de erros, interfaces e idiomas: o percurso Clean Go Code tem exercícios avaliados sobre
errors.Is/errors.As, interfaces pequenas e o gosto no design de que este artigo fala. - O roteiro completo do LevelUpGo mostra como os cursos avançam dos fundamentos ao design de nível sénior.
Começa o curso grátis, escreve Go todos os dias até à entrevista e vais entrar capaz de explicar o teu raciocínio em vez de tentares adivinhar.
