Aprender a programar servicios en segundo plano, conocidos como workers, es fundamental para desarrollar aplicaciones eficientes y rápidas. Los workers permiten ejecutar acciones específicas en respuesta a eventos determinados, volviendo tu aplicación más versátil y eficiente.
¿Qué son los workers en programación y para qué sirven?
Los workers son servicios latentes que esperan eventos específicos para activarse y realizar tareas particulares. Algunas aplicaciones prácticas de los workers incluyen:
Limpieza periódica de un disco duro.
Respuesta a eventos en aplicaciones distribuidas.
Ejecución de actividades específicas bajo determinadas condiciones.
Gracias a su naturaleza de segundo plano, los workers aportan eficiencia y permiten trabajar de manera asíncrona con menor impacto en el rendimiento general.
¿Cómo se implementa un worker en Go usando canales y goroutines?
La creación de un worker en Go requiere definir claramente tres elementos fundamentales:
Un identificador único para la tarea.
Un canal de tipo entero que recibirá tareas.
Un canal de tipo entero que enviará resultados al proceso principal.
Esta implementación permite que múltiples workers trabajen simultáneamente, con la posibilidad de desplegar información sobre el estado de cada uno.
¿Cuál es la importancia del manejo correcto de canales y goroutines?
Programar correctamente los canales y las goroutines determina la claridad y efectividad de los resultados obtenidos. Es necesario:
Asegurar que los canales sean correctamente abiertos y cerrados con close() para evitar problemas de ejecución.
Utilizar bucles de control adecuados para gestionar correctamente las interacciones entre tareas, resultados y workers.
Un ejemplo de manejo correcto sería:
funcmain(){ numeroTareas :=5 tareas :=make(chanint, numeroTareas) resultados :=make(chanint, numeroTareas)for w :=0; w <3; w++{goworker(w, tareas, resultados)}for tarea :=1; tarea <= numeroTareas; tarea++{ tareas <- tarea
}close(tareas)for r :=1; r <= numeroTareas; r++{<-resultados
}}
Aquí se destaca la importancia de cerrar adecuadamente los canales para asegurar el flujo de información.
¿Por qué los resultados de workers pueden aparecer en desorden?
Cuando múltiples goroutines operan simultáneamente, los resultados no siempre mantienen el orden esperado, debido al trabajo asincrónico. Este aspecto es clave al diseñar y entender aplicaciones concurrentes:
La información podría ejecuarse sin respetar el orden FIFO (First In, First Out).
Este desorden es inherente y debe considerarse al planificar el diseño.
Utilizar sentencias como select o implementar timeouts puede ayudarte a controlar este comportamiento, brindando más estructura y claridad a los resultados obtenidos.
¿Has trabajado anteriormente con workers y goroutines en Go? Comparte tu experiencia y dudas en los comentarios.
Sugiero una explicación mucho más detallada acerca de los tipo de canales. Entiendo que se tienen canales bidireccionales (chan), canales de solo recepción (<- chan) y canales de sólo envío (chan <-). En tu función worker utilizas un parámetro tareas de tipo chan, pero no explicas que este es de sólo recepción. Asimismo utilizas otro llamadao resultados, siendo este bidireccional. Me confundió un poco tu explicación, pero indagando en la documentación oficial pude resolver mi duda. Lo dejo anotado para futuras referencias. Gracias.
La sentencia defer en Go se utiliza para posponer la ejecución de una función hasta que la función que la contiene ha finalizado. Esto es útil para garantizar que ciertas operaciones se realicen, como cerrar canales o liberar recursos, independientemente de cómo termine la ejecución de la función.
En el contexto de la clase, donde se utilizan goroutines y canales, defer puede ayudar a cerrar los canales una vez que ya no se necesiten, incluso si la función se interrumpe debido a un error o un retorno anticipado. Por ejemplo:
funcworker(tareas chanint, resultados chanint){deferclose(resultados)// Asegura que el canal resultados se cierra al finalfor tarea :=range tareas {// Procesar la tarea resultados <- tarea *2// Ejemplo de procesamiento}}
Aquí, defer close(resultados) asegura que el canal resultados se cierre al final de la función worker, incluso si se producen errores en el proceso. Esto es especialmente importante cuando las goroutines están trabajando de manera asíncrona y no siempre se tiene control directo sobre el flujo de ejecución.
Es importante notar que los workers son un patrón de diseño. Nos sirve cuando queremos procesar tareas de forma concurrente y cada tarea toma algo de tiempo (por ejemplo, procesar imágenes, calcular datos o realizar consultas I/O).
La idea es tener un grupo fijo de goroutines (los workers) que escuchan tareas desde un canal compartido, procesan una por una y luego reportan los resultados. Esto ayuda a controlar el uso de recursos, evita crear miles de goroutines descontroladas, y mejora el rendimiento.
¿Y por qué no simplemente lanzar una goroutine por cada tarea?
Porque en muchos casos, como los CPU-bound, lanzar muchas goroutines satura el procesador y empeora el rendimiento. En casos I/O-bound, si bien es más tolerable, tener un número fijo de workers aún permite gestionar mejor el flujo y el control de errores.
Además, cerrar el canal de tareas (con close(tareas)) le avisa a los workers que no vendrán más tareas, evitando que se queden en segundo plano ocupando memoria o CPU innecesariamente.
Y por si alguien no lo recuerda o no lo sabe
¿Cuál es la diferencia entre IO Bound y CPU?
La diferencia es qué parte del sistema es el cuello de botella.
Un proceso I/O-bound pasa la mayor parte del tiempo esperando operaciones externas como lecturas de archivos, acceso a bases de datos o peticiones de red (por ejemplo, descargar imágenes de internet).
Un proceso CPU-bound utiliza intensivamente el procesador realizando cálculos complejos o transformaciones de datos (como comprimir archivos o procesar grandes volúmenes de números).
Por eso es especialmente importante usar workers para procesos CPU-bound.
Seria muy bueno algo mas real, por ejemplo consultar 5 servicios rest al tiempo, unir la respuesta y presentar en pantalla una unica respuesta, asi logra que el ejemplo sea mas claro
Tu tranquilo, pon más comentarios de estos para que nos hagamos el curso de tópicos avanzados de Go!!! Jajajajaja
Sintaxis de los channels:
Canal bidireccional(puede enviar y recibir datos)
chan int
Solo lectura(solo puedes recibir datos)
<-chan int
Solo escritura(Solo puedes enviar datos)
chan<- int
defer
La sentencia defer en Go se utiliza para programar una llamada a función (la función diferida) para que se ejecute inmediatamente antes de que la función que contiene eldeferretorne. Es una característica distintiva de Go para manejar situaciones como la liberación de recursos, asegurando que se realice la limpieza necesaria independientemente de la ruta que tome la función para retornar.
Características y Comportamiento:
Evaluación de argumentos: Los argumentos de la función diferida (incluido el receptor si es un método) se evalúan en el momento en que se ejecuta la sentenciadefer, no cuando la llamada real a la función diferida se produce. Esto significa que los valores de las variables capturadas se fijan en el momento del defer.
Orden de ejecución (LIFO): Las funciones diferidas se ejecutan en orden LIFO (último en entrar, primero en salir). Si se difieren múltiples funciones, la última en ser diferida será la primera en ejecutarse.
Momento de ejecución: Las funciones diferidas se ejecutan justo antes de que la función envolvente retorne, ya sea por una sentencia return explícita, por alcanzar el final de su cuerpo, o porque la goroutine correspondiente está en estado de "panic". Si la función envolvente retorna a través de un return explícito, las funciones diferidas se ejecutan después de que cualquier parámetro de resultado haya sido establecido por la sentencia return, pero antes de que la función retorne a su invocador. Si la función diferida tiene valores de retorno, estos se descartan.
Buenas Prácticas y Casos de Uso Comunes paradefer:
Liberación de recursos: Es su uso canónico. Permite que la sentencia de cierre de un recurso (como un archivo o un mutex) se coloque cerca de su apertura o adquisición, garantizando que el recurso se libere incluso si la función tiene múltiples puntos de salida o encuentra un error.
Ejemplo: Cierre de archivos (f.Close()) o desbloqueo de mutexes (l.Unlock()).
Manejo depanicyrecover:defer es crucial para la función recover. recover solo es útil dentro de funciones diferidas y permite que un programa recupere el control de una goroutine que está entrando en pánico, evitando que todo el programa termine. Esto es especialmente útil en servidores para aislar goroutines fallidas.
Trazabilidad/Debugging: Se puede usar para registrar entradas y salidas de funciones (trazas), lo que ayuda a depurar el flujo de ejecución.
close
La función incorporada close se utiliza para registrar que no se enviarán más valores en un canal.
Características y Comportamiento:
Señalización de fin de envío:close indica a los receptores que el canal ha sido cerrado y que no se esperan más envíos.
Comportamiento de la recepción después declose: Después de llamar a close, y una vez que se hayan recibido los valores enviados previamente, las operaciones de recepción en ese canal retornarán el valor cero para el tipo del canal sin bloquearse.
Comprobación de cierre: La forma de asignación multi-valor del operador de recepción (v, ok := <-ch) puede usarse para determinar si un valor recibido fue enviado con éxito o si es un valor cero generado porque el canal está cerrado y vacío (ok será false si el canal está cerrado y vacío).
Uso con buclesfor...range: En un bucle for...range sobre un canal, la iteración continúa recibiendo valores hasta que el canal se cierra. closees fundamental para terminar limpiamente estos bucles.
Condiciones de error (pánico):
Es un error intentar close un canal de solo recepción.
Enviar o cerrar un canal ya cerrado causará un "panic" en tiempo de ejecución.
Cerrar un canal nil también causará un "panic" en tiempo de ejecución.
Buenas Prácticas paraclose:
Cierre de canales para señalización: Utiliza close para señalar a las goroutines que están recibiendo de un canal que no habrá más datos, permitiéndoles terminar sus bucles for...range de forma limpia.
Evitar fugas de goroutines: Asegúrate de que las goroutines que están esperando en un canal tengan una forma de salir. Si una goroutine está bloqueada indefinidamente esperando en un canal que nunca se cierra o que no tiene un remitente, puede resultar en una fuga de goroutine. El uso de select con una cláusula default o un canal de tiempo de espera (time.After) o un canal de "quit" puede ayudar a hacer las goroutines más receptivas y evitar bloqueos indefinidos.
PreferirCloseexplícitos sobre finalizadores/cleanups para recursos no relacionados con la memoria: Aunque Go tiene finalizadores y cleanups, las fuentes recomiendan encarecidamente proporcionar una API explícita (como un métodoClose) para liberar recursos no relacionados con la memoria (como descriptores de archivo) de forma determinista. Los finalizadores y cleanups deben considerarse como un último recurso o un manejador de "mejor esfuerzo" para errores de programación, ya que su tiempo de ejecución no es predecible y no hay garantía de que se ejecuten al salir del programa.
Preferircleanupsafinalizers(Go 1.24+): Para usos en Go 1.24 y versiones posteriores, se recomienda usar cleanups porque son más flexibles, menos propensos a errores y más eficientes que los finalizadores. Los finalizadores pueden resucitar el objeto al que están adjuntos, lo que puede impedir que la memoria sea liberada si el objeto forma parte de un ciclo de referencia.
Pruebas: Al probar código que utiliza cleanups, weak pointers o finalizers, es recomendable:
Evitar ejecutar las pruebas en paralelo.
Usar runtime.GC() para forzar la recolección de basura y la cola de cleanups/finalizers.
Inyectar un mecanismo (como un canal) para bloquear la prueba hasta que la limpieza haya terminado, o bien comprobar el estado post-limpieza en un bucle.
Ejecutar las pruebas en modo race para descubrir condiciones de carrera.
Principio Fundamental de Concurrencia en Go
Un principio clave en Go para el manejo de la concurrencia es: "No comunicarse compartiendo memoria; en su lugar, compartir memoria comunicándose". Esto significa que, en lugar de usar bloqueos y mutexes para proteger el acceso a datos compartidos, se fomenta el paso de datos entre goroutines a través de canales, donde solo una goroutine tiene acceso al valor en un momento dado, eliminando las condiciones de carrera por diseño. Herramientas como el detector de carreras (go build -race) son fundamentales para identificar y diagnosticar condiciones de carrera en programas Go.
Esto es un patron de diseño muy popular en GO.
El uso de las Goroutines me dejaron con dudas. Investigue como controlar las rutinas, porque de que me sirve tener un vehículo sin frenos.
Realice el siguiente ejercicio:
Descargar las imágenes de los Pokemon de la primera generación.
El usuario puede pausar, reanudar y cancelar la rutina.
Para detectar las teclas presionadas por el usuario, utilice un paquete de tercero, si quieres hacer una prueba local del código deberás instalar este paquete con el siguiente comando.
go get github.com/mattn/go-tty
El código es un poco extenso para esta ventana, puedes observar el código desde le siguiente enlace:
Cualquier aporte o comentario, sera un placer leerlo.
No se cargo la imagen 😭, aunque el preview se veía.
Los workers en Go, utilizando goroutines y channels, son ideales para tareas que requieren procesamiento en segundo plano. Algunos casos reales incluyen:
Procesamiento de datos: Manejo de grandes volúmenes de datos en aplicaciones como análisis de logs o generación de reportes.
Servicios programados: Tareas como backups automáticos o limpieza de bases de datos.
Interacción con APIs: Realizar llamadas a APIs externas sin bloquear el hilo principal de la aplicación, mejorando la eficiencia.
Notificaciones: Envío de correos o mensajes en segundo plano mientras se ejecutan otras tareas.
Estos ejemplos demuestran cómo la concurrencia en Go puede optimizar el rendimiento de aplicaciones backend.