Tema representativo de entrevista

Entrevista técnica: ¿Cómo mantiene Go 1.25 WaitGroup.Go correctos los ciclos de vida de las tareas concurrentes?

CodingDifícil
Equipo editorial de Offer.ccPublicado Actualizado

Pregunta

Refactoriza un procesador por lotes concurrente con Go 1.25 sync.WaitGroup.Go. ¿Cómo logras que el conteo, la espera, el manejo de pánicos, la cancelación de contexto y la recolección de errores sean correctos, y cuándo seguirías eligiendo errgroup?

Planteamiento y contexto

Un procesador por lotes maneja muchos objetos de forma concurrente. La goroutine principal espera a todas las tareas y detiene el despacho cuando una tarea falla o el contexto se cancela. Usa Go 1.25 sync.WaitGroup.Go para definir el ciclo de vida, y explica su relación con Add, Done y Wait, su contrato de pánico, la propagación de errores y los límites de cancelación. Esta es una pregunta de coding porque la habilidad central es el diseño del ciclo de vida de tareas concurrentes.

Qué evalúa el entrevistador

Primero, si vinculas la creación de la tarea con el conteo en lugar de ubicar Add donde pueda generar una condición de carrera con Wait.

Segundo, si comprendes el contrato de WaitGroup.Go: la función no debe entrar en pánico, y el contador se decrementa cuando esta retorna; no se proporciona propagación de errores ni de cancelación.

Tercero, si puedes diseñar concurrencia acotada, detener el despacho y recolectar resultados sin fugas, condiciones de carrera ni colas ilimitadas.

Cuarto, si puedes identificar cuándo errgroup.WithContext se adapta mejor que tratar a WaitGroup como una primitiva de orquestación completa.

Quinto, si las pruebas cubren casos de éxito, cancelación, protección contra pánicos y carreras de espera, manteniendo explícitos la versión de Go y el contrato de CI.

Preguntas para aclarar primero

  • ¿El compilador está fijado a Go 1.25 o superior?
  • ¿Cuál es el límite de concurrencia y si la entrada puede ser un flujo ilimitado?
  • ¿El primer error debe cancelar a las tareas hermanas, o se deben recolectar todos los errores?
  • ¿Puede una tarea entrar en pánico y, de ser así, qué capa lo recupera?
  • ¿Los resultados deben preservar el orden de entrada y los errores necesitan identificadores de objeto?
  • ¿Las llamadas downstream soportan cancelación por contexto y reintentos idempotentes?

Una respuesta en 30 segundos

“Uso WaitGroup.Go para vincular cada lanzamiento con su contador y un semáforo para limitar la concurrencia. Cada tarea verifica el contexto y escribe resultados o errores mediante un recolector protegido. WaitGroup solo espera; no propaga errores ni cancelación, por lo que el primer error debe cancelar explícitamente un contexto derivado. Si la política estándar es cancelar ante el primer error, usaría errgroup.WithContext. El punto de entrada de la tarea debe recuperar los pánicos permitidos convirtiéndolos en errores o hacer explícita la política a nivel de proceso. Las pruebas cubren cancelación, el pico de concurrencia, la convergencia y las carreras.”

Solución detallada

Paso 1: Fijar el contrato de la API

Go 1.25 agregó Go(f func()) a sync.WaitGroup. Inicia f y realiza el equivalente a Done cuando f retorna; la documentación exige que f no entre en pánico. Fija la versión en go.mod, imágenes de CI y herramientas locales para que compiladores más antiguos no ingresen silenciosamente al flujo de trabajo.

Paso 2: Colocar el límite de concurrencia en el límite de la tarea

Usa un semáforo con búfer antes de llamar a Go o dentro de la tarea. Si el despacho puede bloquearse, haz que la adquisición sea cancelable para que un contexto cancelado no deje al despachador esperando indefinidamente.

go
var wg sync.WaitGroup
sem := make(chan struct{}, 8)
for _, item := range items {
  if err := ctx.Err(); err != nil { break }
  sem <- struct{}{}
  item := item
  wg.Go(func() {
    defer func() { <-sem }()
    if ctx.Err() != nil { return }
    process(item)
  })
}
wg.Wait()

Paso 3: Definir canales de error y cancelación

WaitGroup no almacena errores ni cancela tareas hermanas. Deriva un contexto cancelable; la primera tarea que registre un error llama a la cancelación. Usa un canal o un mutex para el recolector y léelo después de que Wait retorne para evitar accesos concurrentes.

Paso 4: Hacer explícito el límite de pánico

Si el servicio trata el pánico como una falla recuperable de la tarea, un recover diferido puede convertirlo en un error con seguimiento de pila y activar la cancelación. Si el pánico indica una invariante rota, no lo recuperes silenciosamente; documenta el registro en logs, las alertas y el comportamiento de salida del proceso.

Paso 5: Evitar carreras entre despacho y espera

No llames a Wait mientras otra goroutine aún pueda llamar a Go a menos que un protocolo de ciclo de vida lo haga seguro. Un procesador por lotes debe tener un despachador que decida cuándo no se agregan más tareas y luego llame a Wait. Las tareas recursivas dinámicas deben definir cuándo se permiten llamadas anidadas a Go.

Paso 6: Comparar con errgroup

errgroup.WithContext proporciona el primer error no nulo, cancelación derivada y un límite opcional, por lo que se ajusta a la dispersión de solicitudes (fan-out) donde un fallo detiene a las demás tareas. WaitGroup.Go se adapta a tareas independientes, agregación de errores personalizada o ciclos de vida gestionados por otro componente.

Paso 7: Verificar las invariantes

Las pruebas deben demostrar que cada tarea iniciada finalmente decrementa el contador; la cancelación detiene nuevos despachos; un error activa la cancelación una sola vez; el pico se mantiene dentro del límite; y los resultados no cambian después de Wait. Ejecuta go test -race para detectar condiciones de carrera en el recolector.

Un ejemplo de respuesta de alta calidad

“Primero fijo Go 1.25. El despachador adquiere un semáforo mientras el contexto está activo y luego llama a wg.Go; la tarea libera el semáforo, verifica la cancelación y realiza el trabajo. WaitGroup solo proporciona la espera del ciclo de vida, por lo que agrego un contexto derivado, un canal de errores que transporta el ID del objeto y una cancelación de un solo disparo para la política de primer error. Si el pánico es recuperable, lo convierto en un error con su traza de pila; de lo contrario, el manejo a nivel de proceso permanece explícito. Detengo el despacho antes de Wait, agrego los resultados después y ejecuto go test -race. Para cancelación ante el primer error junto con un límite, elegiría errgroup.WithContext.”

Errores comunes

  • Tratar WaitGroup.Go como un error group → los errores no se propagan → recolectarlos explícitamente o usar errgroup.
  • Bloquearse en el semáforo tras la cancelación → el despachador no puede salir → usar select con el contexto durante la adquisición.
  • Dejar que la tarea entre en pánico directamente → viola el contrato y puede abortar el proceso → convertir pánicos permitidos o usar una política de proceso explícita.
  • Hacer append a un solo slice desde múltiples goroutines → produce una condición de carrera de datos → usar un canal, mutex o agregación posterior a la espera.
  • Llamar a Wait antes de finalizar el despacho → las adiciones dinámicas compiten con la espera → definir un protocolo de detención de despacho.
  • Verificar únicamente el conteo final → oculta errores de cancelación y de pico de concurrencia → comprobar cada invariante y ejecutar el detector de carreras.
  • Ignorar la versión de Go → el comportamiento local y el de CI divergen → fijar el módulo, la imagen y la cadena de herramientas.
  • Usar WaitGroup para cada aspecto de la orquestación → los reintentos, plazos límite y políticas de primer error quedan dispersos → elegir errgroup o un planificador dedicado cuando corresponda.

Preguntas de seguimiento

Pregunta de seguimiento 1: ¿Puede Go recibir una función que entre en pánico?

La documentación oficial exige que la función no entre en pánico. Si el pánico es una falla de negocio recuperable, recupéralo en un límite definido y retorna un error; de lo contrario, conserva la semántica de pánico y apóyate en la política de recuperación y alertas a nivel de proceso.

Pregunta de seguimiento 2: ¿Cómo garantizas que ninguna tarea inicie tras la cancelación?

Verifica ctx.Err() antes del despacho, usa un select cancelable mientras adquieres el semáforo y verifica el contexto nuevamente al entrar a la tarea. El trabajo que ya se está ejecutando aún depende de que las APIs downstream respeten la cancelación.

Pregunta de seguimiento 3: ¿Cuándo es mejor errgroup?

Elige errgroup.WithContext para propagación del primer error, cancelación de tareas hermanas y espera unificada. Elige WaitGroup.Go para tareas independientes o agregación de errores personalizada.

Pregunta de seguimiento 4: ¿Se puede llamar a Go mientras se espera?

Solo con un protocolo de ciclo de vida que evite que el contador llegue a cero antes de que se agregue nuevo trabajo. Un procesamiento por lotes normal debe detener el despacho antes de Wait para evitar carreras con el contador en cero.

Pregunta de seguimiento 5: ¿Cómo preservas el orden de los resultados?

Asigna a cada entrada un índice y haz que la tarea escriba en su posición asignada o envíe un resultado indexado. Agrega por índice después de esperar; no compartas un slice solo de adición entre tareas.

Pregunta de seguimiento 6: ¿Cómo pruebas la recuperación de pánicos?

Inyecta una tarea que entre en pánico y verifica que la capa de recuperación emita un error identificado, active la cancelación y permita que las demás tareas converjan. Prueba por separado la ruta sin recuperación contra la política acordada de monitoreo y proceso.

Fuentes públicas

Preguntas relacionadas

Herramienta de entrevista relacionada

Usa Captura para un ejercicio de código

Captura el problema y luego aborda en orden las restricciones, la solución, el código, los casos extremos y la complejidad.

Ver la herramienta