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.

Go Technical Interview: Goroutines, Channels and Concurrency

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).

goroutine_basics.gogo
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.

panic_recovery.gogo
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.

channel_semantics.gogo
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.

close_channel.gogo
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.

select_multiplex.gogo
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.

fanout_fanin.gogo
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.

worker_pool.gogo
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.

race_condition.gogo
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.

context_cancellation.gogo
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 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.

deadlock_example.gogo
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.

rate_limited.gogo
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 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

¡Empieza a practicar!

Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.

Etiquetas

#go
#interview
#concurrency
#goroutines
#channels

Compartir

Artículos relacionados