# 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. - Published: 2026-04-01 - Updated: 2026-04-01 - Author: Anthony Fillion-Maillet - Tags: go, interview, concurrency, goroutines, channels - Reading time: 10 min --- 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. > **Lo que realmente evaluan los entrevistadores** > > 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). ```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") } ``` 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. ```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 {} } ``` 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. ```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 } ``` 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. ```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) } ``` 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. ## 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. ```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) } ``` 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. ```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) } } ``` 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. ```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) } } ``` 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. ```go // race_condition.go 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. ```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) } } ``` 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. > **Trampa comun en entrevistas** > > Nunca se debe almacenar un `context.Context` en un campo de struct. La [documentacion oficial de Go](https://pkg.go.dev/context) 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. ```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" } ``` 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. > **Los deadlocks parciales son invisibles** > > 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. ```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() } ``` 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. ## 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 `select` multiplexa operaciones de channels con seleccion aleatoria cuando multiples estan listos; se combina con `context.Context` para 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.Mutex` para estado compartido complejo, `sync/atomic` para contadores simples y channels para la comunicacion entre goroutines - Ejecutar `go test -race` en CI es indispensable para detectar data races; los deadlocks parciales requieren `pprof` para su diagnostico - Nunca se debe almacenar `context.Context` en 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 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/es/blog/go/go-technical-interview-goroutines-channels-concurrency