Entrevista Tecnica de Go: Goroutines, Channels y Concurrencia
Guia completa con las preguntas mas frecuentes sobre goroutines, channels y concurrencia en Go. Incluye ejemplos de codigo listos para produccion, patrones de concurrencia y las respuestas que los entrevistadores esperan en 2026.

Las preguntas de entrevistas tecnicas sobre goroutines, channels y concurrencia en Go figuran de manera consistente entre los temas mas desafiantes que enfrentan los candidatos. Comprender estos conceptos a un nivel profundo es lo que distingue a un ingeniero Go senior de quienes todavia estan aprendiendo el lenguaje. Esta guia cubre las preguntas exactas que formulan los entrevistadores en 2026, con ejemplos de codigo de calidad profesional y el razonamiento detras de cada respuesta.
Las entrevistas de concurrencia en Go se centran en tres areas: gestion del ciclo de vida de goroutines, semantica de channels (buffered vs unbuffered, tipos direccionales) y composicion de patrones (fan-out/fan-in, worker pools, cancelacion con context). Memorizar sintaxis resulta insuficiente: los entrevistadores esperan que los candidatos razonen sobre race conditions y deadlocks.
Fundamentos de Goroutines: Lo que Todo Entrevistador Pregunta
La primera ronda de preguntas suele explorar si el candidato comprende realmente que son las goroutines, no solo como iniciarlas.
P: Que es una goroutine y en que se diferencia de un hilo del sistema operativo?
Una goroutine es una funcion concurrente liviana administrada por el scheduler del runtime de Go, no por el sistema operativo. El runtime de Go multiplexa miles de goroutines sobre un pequeno numero de hilos del sistema operativo utilizando un modelo de scheduling M:N (M goroutines mapeadas a N hilos del 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")
}Diferencias clave que conviene mencionar en una entrevista: las goroutines comienzan con un stack de 2-8KB que crece de forma dinamica, en contraste con el stack fijo de 1-8MB de un hilo del SO. El cambio de contexto entre goroutines se maneja en espacio de usuario por el scheduler de Go, lo cual evita los costosos cambios de contexto a nivel de kernel propios de los hilos del SO. Esto hace que lanzar 100,000 goroutines sea totalmente practico, mientras que 100,000 hilos del SO agotarian los recursos del sistema.
P: Que sucede si una goroutine entra en panic?
Un panic no recuperado en cualquier goroutine causa el crash de todo el programa. A diferencia de las excepciones en Java o Python, un panic se propaga por el call stack de la propia goroutine, no por el stack de la goroutine que la inicio. La unica forma de capturarlo es con recover() dentro de una funcion diferida (defer) en la misma goroutine.
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 {}
}Los entrevistadores buscan que el candidato sepa que los servicios Go en produccion envuelven el lanzamiento de goroutines en un patron de recuperacion. Librerias como errgroup manejan esto de manera mas elegante.
Semantica de Channels: Buffered, Unbuffered y Direccionales
Las preguntas sobre channels revelan si un candidato comprende verdaderamente el modelo de concurrencia de Go o solo memoriza patrones.
P: Cual es la diferencia entre un channel buffered y uno unbuffered?
Un channel unbuffered (make(chan T)) requiere que tanto el emisor como el receptor esten listos simultaneamente: el envio se bloquea hasta que otra goroutine recibe. Un channel buffered (make(chan T, n)) permite enviar hasta n valores sin bloquearse.
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
}La pregunta de seguimiento que los entrevistadores suelen formular es: "Cuando elegirias uno sobre el otro?" Los channels unbuffered imponen sincronizacion, lo cual resulta util cuando el emisor necesita saber que el receptor proceso el valor. Los channels buffered desacoplan la sincronizacion temporal entre emisor y receptor, lo cual es util para colas de trabajo o limitacion de tasa donde cierta holgura es aceptable.
P: Que ocurre cuando se cierra un channel?
Cerrar un channel indica que no se enviaran mas valores. Las recepciones sobre un channel cerrado retornan inmediatamente con el valor zero del tipo. Enviar sobre un channel cerrado provoca un panic. Un bucle range sobre un channel finaliza cuando el channel se cierra.
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)
}Un punto critico: solo el emisor debe cerrar un channel, nunca el receptor. Cerrar un channel en el que otra goroutine todavia esta escribiendo provoca un panic.
¿Listo para aprobar tus entrevistas de Go?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
La Sentencia Select: Multiplexando Channels
P: Como funciona select y que sucede cuando multiples cases estan listos?
La sentencia select se bloquea hasta que una de sus operaciones de channel pueda proceder. Cuando multiples cases estan listos simultaneamente, Go elige uno de forma aleatoria, lo cual previene la inanicion de cualquier case en particular.
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)
}Los entrevistadores evaluan dos aspectos con select: la comprension de la regla de seleccion aleatoria y la capacidad de combinar channels con context.Context para patrones de timeout y cancelacion.
Patrones de Concurrencia Frecuentes en Entrevistas
Las entrevistas de Go para nivel senior casi siempre incluyen una pregunta sobre la implementacion desde cero de alguno de estos patrones.
Patron Fan-Out/Fan-In
P: Implemente un pipeline fan-out/fan-in que procese elementos de forma concurrente.
Fan-out distribuye el trabajo entre multiples goroutines. Fan-in recopila los resultados de multiples goroutines en un solo channel.
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)
}
}El punto clave que los entrevistadores buscan: el channel del generator es compartido entre c1 y c2, por lo que cada valor es procesado por exactamente un worker (no se duplica). La funcion fanIn utiliza un WaitGroup para saber cuando todos los channels de entrada han sido drenados antes de cerrar el channel unificado.
Worker Pool con errgroup
P: Como implementaria un worker pool con limite de concurrencia y manejo de errores?
El paquete golang.org/x/sync/errgroup (parte de la biblioteca estandar extendida de Go) resuelve esto de forma limpia. Administra el ciclo de vida de las goroutines, recopila el primer error y se integra con context para la cancelacion.
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)
}
}Desde Go 1.24 (la version estable actual a principios de 2026), este patron sigue siendo el enfoque recomendado. El metodo SetLimit fue agregado en Go 1.20 y elimina la necesidad de implementar manualmente la limitacion de concurrencia basada en semaforos.
Race Conditions y Primitivas de sync
P: Como se detectan y previenen las race conditions en Go?
Go proporciona un detector de race conditions integrado que se activa con la flag -race. Detecta accesos concurrentes no sincronizados a memoria compartida en tiempo de ejecucion.
package main
import (
"fmt"
"sync"
"sync/atomic"
)
// BAD: race condition — 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
}La respuesta en una entrevista debe cubrir tres estrategias de sincronizacion: sync.Mutex / sync.RWMutex para estado compartido complejo, sync/atomic para contadores y flags simples, y channels para la comunicacion entre goroutines ("compartir memoria comunicando, no comunicar compartiendo memoria"). Ejecutar go test -race ./... debe ser parte de todo pipeline de CI.
Context y Patrones de Cancelacion
P: Explique como context.Context controla el ciclo de vida de las goroutines.
El paquete context proporciona un mecanismo para propagar senales de cancelacion, deadlines y valores de alcance por solicitud a traves de los limites de las goroutines. Toda goroutine de larga duracion debe aceptar un context.Context como su primer parametro.
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)
}
}Los workers 1 y 2 se completan dentro del deadline de 250ms. Los workers 3, 4 y 5 reciben la senal de cancelacion a traves de ctx.Done(). Este patron es fundamental para construir servidores HTTP resilientes y microservicios en Go: cada manejador de solicitudes recibe un context que propaga la cancelacion cuando el cliente se desconecta.
Nunca se debe almacenar un context.Context en un campo de struct. La documentacion oficial de Go establece explicitamente: "No almacene Contexts dentro de un tipo struct; en su lugar, pase un Context de forma explicita a cada funcion que lo necesite." Los entrevistadores verifican esto para evaluar si el candidato sigue las convenciones de Go.
Deteccion de Deadlocks: Preguntas Capciosas en Entrevistas
P: Este codigo provocara un deadlock? Por que?
Las preguntas sobre deadlocks son populares porque evaluan la capacidad del candidato para razonar sobre el scheduling de goroutines y las operaciones de channels.
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"
}La solucion es directa: hacer el channel buffered (make(chan int, 1)) o lanzar una goroutine para recibir antes de enviar. El runtime de Go detecta deadlocks cuando todas las goroutines estan bloqueadas, pero solo cuando todas las goroutines estan dormidas. Si aunque sea una goroutine esta ejecutandose (por ejemplo, un servidor HTTP en segundo plano), el runtime no detectara un deadlock parcial.
El runtime de Go solo detecta deadlocks cuando cada goroutine del programa esta bloqueada. En aplicaciones reales con servidores HTTP o workers en segundo plano, las goroutines filtradas que estan en deadlock no activaran el detector del runtime. Herramientas como pprof y los dumps de goroutines (runtime.Stack) son necesarias para diagnosticar estos problemas en produccion.
Patron Avanzado: Procesamiento Concurrente con Limite de Tasa
P: Como implementaria llamadas a API concurrentes con limite de tasa?
Esta pregunta evalua la capacidad de combinar multiples primitivas de concurrencia en una solucion cohesiva.
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()
}Este patron combina un semaforo basado en channels (para limitar la concurrencia) con un ticker (para limitar la tasa). El doble select con verificacion de contexto garantiza un apagado graceful. Este es el tipo de respuesta lista para produccion que distingue a los candidatos senior.
¡Empieza a practicar!
Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.
Conclusion
- Las goroutines son hilos en espacio de usuario administrados por el runtime de Go con scheduling M:N; siempre se deben recuperar los panics en goroutines lanzadas
- Los channels unbuffered sincronizan emisor y receptor; los channels buffered desacoplan la sincronizacion temporal. La eleccion depende de si el emisor necesita confirmacion
- La sentencia
selectmultiplexa operaciones de channels con seleccion aleatoria cuando multiples estan listos; se combina concontext.Contextpara timeouts - Fan-out/fan-in y worker pools (via
errgroup.SetLimit) son los dos patrones de concurrencia que se preguntan con mayor frecuencia - Se utiliza
sync.Mutexpara estado compartido complejo,sync/atomicpara contadores simples y channels para la comunicacion entre goroutines - Ejecutar
go test -raceen CI es indispensable para detectar data races; los deadlocks parciales requierenpprofpara su diagnostico - Nunca se debe almacenar
context.Contexten structs: se pasa como primer parametro de funcion - El rate limiting en Go combina semaforos de channels con tickers, envueltos en sentencias select conscientes del contexto
¡Empieza a practicar!
Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.
Etiquetas
Compartir
Artículos relacionados

Concurrencia en Go: Goroutines y Canales - Guía Completa
Domina la concurrencia en Go con goroutines y canales. Patrones avanzados, sincronización, sentencias select y mejores prácticas con ejemplos de código detallados.

Top 25 preguntas de entrevista Go: guía completa para desarrolladores
Domina las entrevistas de Go con las 25 preguntas más frecuentes. Goroutines, channels, interfaces y patrones de concurrencia con ejemplos de código.

Go: Fundamentos para Desarrolladores Java/Python en 2026
Aprende Go rápidamente aprovechando tu experiencia en Java o Python. Goroutines, channels, interfaces y patrones esenciales para una transición fluida.