{"id":24429037,"url":"https://github.com/dieg0code/golang_concurrecy","last_synced_at":"2026-03-16T08:04:43.668Z","repository":{"id":271962626,"uuid":"915100145","full_name":"Dieg0Code/golang_concurrecy","owner":"Dieg0Code","description":"Personal Notes","archived":false,"fork":false,"pushed_at":"2025-01-11T02:57:02.000Z","size":291,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-20T13:35:18.540Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Dieg0Code.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-01-11T01:06:39.000Z","updated_at":"2025-01-11T02:57:05.000Z","dependencies_parsed_at":"2025-01-20T13:33:35.287Z","dependency_job_id":null,"html_url":"https://github.com/Dieg0Code/golang_concurrecy","commit_stats":null,"previous_names":["dieg0code/golang_concurrecy"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Dieg0Code%2Fgolang_concurrecy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Dieg0Code%2Fgolang_concurrecy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Dieg0Code%2Fgolang_concurrecy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Dieg0Code%2Fgolang_concurrecy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Dieg0Code","download_url":"https://codeload.github.com/Dieg0Code/golang_concurrecy/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243456566,"owners_count":20293905,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-01-20T13:33:07.865Z","updated_at":"2025-12-27T11:11:41.030Z","avatar_url":"https://github.com/Dieg0Code.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🚀 Concurrencia en Go: Una Guía Completa\n\n## 🗺️ Mapa de Conceptos: Concurrencia en Go\n\nA continuación te presento una visión general de los conceptos fundamentales que exploraremos en este documento:\n\n### 📊 Conceptos Fundamentales\n\n- **Concurrencia vs Paralelismo** ➡️ Gestionar múltiples tareas en progreso vs ejecutar tareas simultáneamente\n- **Goroutines** ➡️ Funciones que se ejecutan concurrentemente, más ligeras que los hilos tradicionales\n- **Sincronización** ➡️ Coordinar la ejecución entre goroutines para evitar problemas\n\n### 🛠️ Herramientas de Sincronización\n\n1. **WaitGroups** ➡️ Esperar a que múltiples goroutines terminen\n2. **Mutexes** ➡️ Proteger el acceso a datos compartidos\n3. **Channels** ➡️ Comunicar y transferir datos entre goroutines\n\n### 🧩 Patrones de Concurrencia\n\n- **Productor-Consumidor** ➡️ Coordinación entre generadores y procesadores de datos\n- **Filósofos Comensales** ➡️ Gestión de recursos compartidos evitando deadlocks\n- **Fan-out/Fan-in** ➡️ Distribución y recolección de trabajo paralelo\n- **Worker Pools** ➡️ Grupo de goroutines que procesan tareas de una cola\n\n### ⚠️ Problemas Comunes\n\n- **Race Conditions** ➡️ Acceso simultáneo descoordinado a datos compartidos\n- **Deadlocks** ➡️ Bloqueo mutuo donde todas las goroutines quedan esperando\n- **Goroutines Huérfanas** ➡️ Goroutines que nunca terminan\n\n### 🔍 Herramientas de Diagnóstico\n\n- **Race Detector** ➡️ `go run -race` o `go test -race` para detectar problemas\n- **Pprof** ➡️ Perfilador para identificar cuellos de botella\n\nEste documento te llevará desde los conceptos básicos hasta patrones avanzados con ejemplos prácticos, consejos de implementación y soluciones a problemas clásicos de concurrencia. Cada sección está diseñada para construir sobre la anterior, proporcionándote una comprensión completa de cómo Go maneja la concurrencia de forma elegante y eficiente.\n\n# 🔄 La Filosofía de Concurrencia en Go: Un Resumen\n\n## 🧠 La Visión de los Creadores\n\nGo fue diseñado desde el principio para abordar los desafíos de la programación concurrente y distribuida en un mundo cada vez más paralelo. Los creadores de Go (Rob Pike, Ken Thompson y Robert Griesemer) tenían una visión clara: **hacer que la concurrencia sea accesible, práctica y segura**.\n\n## 🛠️ Las Herramientas Principales y su Filosofía\n\n### 1️⃣ Goroutines: Concurrencia Ligera\n\n**💡 Idea central**: Hacer que la concurrencia sea tan simple como añadir una palabra clave.\n\n- **Extremadamente ligeras**: Miles de goroutines pueden ejecutarse en un solo hilo del sistema operativo\n- **Programación automática**: El runtime de Go se encarga de distribuir goroutines entre hilos\n- **Sintaxis simple**: Solo añade `go` antes de una llamada a función\n\n```go\ngo function() // ¡Así de simple!\n```\n\n### 2️⃣ Channels: Comunicación Estructurada\n\n**💡 Idea central**: \"No comuniques compartiendo memoria; comparte memoria comunicándote.\"\n\n- **Paso de mensajes tipados**: Transferencia segura de datos entre goroutines\n- **Sincronización integrada**: Coordinación automática entre emisor y receptor\n- **Concepto de propiedad**: Canales facilitan la transferencia clara de \"propiedad\" de datos\n\n```go\nch := make(chan int)\nch \u003c- 42         // Envía dato (puede bloquear)\nvalue := \u003c-ch    // Recibe dato (puede bloquear)\n```\n\n### 3️⃣ Select: Multiplexación Elegante\n\n**💡 Idea central**: Manejar múltiples canales de comunicación de forma no determinista.\n\n- **Coordinación multi-canal**: Esperar por múltiples operaciones de canales\n- **Timeout y cancelación**: Patrones integrados para control de tiempo y finalización\n- **No determinismo controlado**: Selección aleatoria cuando múltiples casos están disponibles\n\n```go\nselect {\n    case msg := \u003c-ch1:\n        // Manejar mensaje de ch1\n    case ch2 \u003c- value:\n        // Enviar a ch2\n    case \u003c-time.After(1 * time.Second):\n        // Timeout después de 1 segundo\n}\n```\n\n### 4️⃣ Paquete sync: Control de Bajo Nivel\n\n**💡 Idea central**: Proporcionar primitivas de sincronización cuando los canales no son suficientes.\n\n- **WaitGroup**: Coordinar la finalización de múltiples goroutines\n- **Mutex/RWMutex**: Proteger acceso a datos compartidos (cuando sea necesario)\n- **Once**: Garantizar que una operación se ejecute exactamente una vez\n- **Pool**: Reutilizar recursos costosos\n- **Cond**: Variables de condición para situaciones complejas\n\n```go\nvar wg sync.WaitGroup\nwg.Add(5)      // Esperaremos 5 goroutines\ngo func() {\n    defer wg.Done()  // Decrementa el contador\n    // ...trabajo...\n}()\nwg.Wait()      // Bloquea hasta que el contador llegue a cero\n```\n\n## 🧩 Cómo Encajan Estas Piezas: La Gran Imagen\n\nLos creadores de Go visualizaron un sistema donde:\n\n1. **Las goroutines son la unidad básica de concurrencia**: pequeñas, independientes y fáciles de crear.\n\n2. **Los channels son la forma primaria de comunicación**: la transferencia explícita de datos es preferible a los estados compartidos y complejos sistemas de bloqueo.\n\n3. **El paquete sync complementa, no reemplaza**: cuando necesitas sincronización de bajo nivel, está disponible pero no es el enfoque principal.\n\n## 📚 Principios Guía\n\n### 1. Simplicidad sobre complejidad\n\nGo evita abstracciones complejas (como promesas, futuros o callbacks anidados) en favor de un modelo mental simple.\n\n### 2. Composición sobre jerarquía\n\nLas goroutines y channels pueden combinarse en patrones potentes sin necesidad de frameworks complejos.\n\n### 3. Explícito es mejor que implícito\n\nLa sincronización y comunicación son claras y visibles en el código, no ocultas en abstracciones.\n\n### 4. Pragmatismo\n\nGo proporciona tanto canales (modelo CSP) como primitivas de sincronización tradicionales (mutex), reconociendo que diferentes problemas necesitan diferentes herramientas.\n\n## 🌉 De la Teoría a la Práctica: Patrones Emergentes\n\nLos patrones de concurrencia en Go surgieron naturalmente de estas herramientas:\n\n- **Worker Pools**: Grupos de goroutines consumiendo tareas de un canal\n- **Fan-out/Fan-in**: Distribución y recolección de trabajo entre múltiples goroutines\n- **Pipelines**: Etapas conectadas por canales para procesar flujos de datos\n- **Cancelación por context**: Propagación de señales de cancelación a través de árboles de llamadas\n\n## 💡 La Clave del Éxito: El Modelo de Concurrencia de Go\n\nLo que hace especial al modelo de concurrencia de Go es su **equilibrio entre poder y simplicidad**:\n\n- Suficientemente **poderoso** para construir sistemas distribuidos complejos\n- Suficientemente **simple** para ser entendido y usado correctamente\n- Suficientemente **seguro** para evitar errores comunes de concurrencia\n- Suficientemente **eficiente** para escalar a miles de goroutines\n\nLa concurrencia en Go no es solo un conjunto de herramientas, sino una **filosofía**:\n\n\u003e \"Go no intenta resolver todos los problemas de concurrencia, pero ofrece un conjunto coherente y práctico de primitivas que permiten abordar una amplia gama de problemas concurrentes de manera eficiente y con menos errores.\"\n\nEsta filosofía pragmática, combinada con herramientas bien diseñadas, hace que la programación concurrente en Go sea notablemente más accesible y robusta que en muchos otros lenguajes.\n\n## 🌟 Introducción a la Concurrencia\n\nLa concurrencia es uno de los puntos fuertes de Go y una razón clave por la que muchos desarrolladores eligen este lenguaje. Pero, ¿qué es exactamente?\n\n**La concurrencia** es la capacidad de un programa para manejar múltiples tareas en progreso al mismo tiempo. Es importante distinguirla del paralelismo:\n\n- **Concurrencia** ➡️ Gestionar múltiples tareas en curso (no necesariamente ejecutándose exactamente al mismo tiempo)\n- **Paralelismo** ➡️ Ejecutar múltiples tareas simultáneamente (requiere múltiples procesadores)\n\n\u003e 💡 **Analogía**: La concurrencia es como un chef que prepara varios platos a la vez. Mientras uno se hornea, está cortando verduras para otro. No está haciendo todo literalmente al mismo tiempo, pero avanza con todas las tareas eficientemente.\n\nEn Go, la concurrencia se implementa principalmente mediante **goroutines** y **channels**. En esta guía nos centraremos en goroutines y su sincronización.\n\n## 🧵 Goroutines: El Núcleo de la Concurrencia en Go\n\nLas goroutines son funciones que se ejecutan concurrentemente con otras goroutines, incluyendo la función principal (`main`). Son extremadamente ligeras - puedes crear miles de ellas sin problemas.\n\n### Creación de Goroutines\n\nPara ejecutar una función como goroutine, simplemente añade la palabra clave `go` antes de la llamada:\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"time\"\n)\n\nfunc main() {\n    // Ejecutamos la función como goroutine\n    go sayHello()\n\n    // Esperamos para dar tiempo a que la goroutine se ejecute\n    time.Sleep(1 * time.Second)\n    fmt.Println(\"Programa terminado\")\n}\n\nfunc sayHello() {\n    fmt.Println(\"¡Hola, mundo concurrente!\")\n}\n```\n\n### ⚠️ El Problema de la Sincronización\n\nEn el ejemplo anterior, usamos `time.Sleep()` para dar tiempo a que la goroutine termine. Esto es una **mala práctica** por varias razones:\n\n1. No sabemos cuánto tiempo exactamente necesita la goroutine\n2. Es ineficiente esperar un tiempo arbitrario\n3. En situaciones complejas, este enfoque se vuelve inmanejable\n\nVeamos otro ejemplo:\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"time\"\n)\n\nfunc printSomething(s string) {\n    fmt.Println(s)\n}\n\nfunc main() {\n    words := []string{\n        \"alpha\", \"beta\", \"gamma\", \"delta\", \"phi\",\n        \"zeta\", \"eta\", \"theta\", \"epsilon\",\n    }\n\n    // Lanzamos múltiples goroutines\n    for i, word := range words {\n        go printSomething(fmt.Sprintf(\"%d: %s\", i, word))\n    }\n\n    // Esperamos arbitrariamente - ¡mala práctica!\n    time.Sleep(1 * time.Second)\n}\n```\n\nAl ejecutar este código, notarás que:\n\n1. Las palabras no se imprimen en orden\n2. No tenemos garantía de que todas las goroutines terminen antes del `time.Sleep`\n\n## 🔄 WaitGroup: Sincronizando Goroutines\n\nPara resolver el problema de sincronización, Go proporciona `sync.WaitGroup`. Un WaitGroup espera a que un conjunto de goroutines finalice, actuando como un contador:\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"sync\"\n)\n\nfunc printSomething(wg *sync.WaitGroup, s string) {\n    // Indicamos que esta goroutine ha terminado cuando la función finalice\n    defer wg.Done()\n    fmt.Println(s)\n}\n\nfunc main() {\n    // Creamos un WaitGroup\n    var wg sync.WaitGroup\n\n    words := []string{\n        \"alpha\", \"beta\", \"gamma\", \"delta\", \"phi\",\n        \"zeta\", \"eta\", \"theta\", \"epsilon\",\n    }\n\n    // Indicamos cuántas goroutines vamos a esperar\n    wg.Add(len(words))\n\n    for i, word := range words {\n        go printSomething(\u0026wg, fmt.Sprintf(\"%d: %s\", i, word))\n    }\n\n    // Bloqueamos hasta que todas las goroutines terminen\n    wg.Wait()\n    fmt.Println(\"¡Todas las goroutines han terminado!\")\n}\n```\n\n### 🛠️ Métodos principales de WaitGroup\n\n1. **`wg.Add(n)`**: Incrementa el contador en `n` (número de goroutines a esperar)\n2. **`wg.Done()`**: Decrementa el contador en 1 (una goroutine ha terminado)\n3. **`wg.Wait()`**: Bloquea hasta que el contador llegue a 0 (todas han terminado)\n\n\u003e ⚠️ **Advertencia**: Asegúrate de que los valores de `Add()` y `Done()` estén equilibrados. Un exceso de `Add()` causará un deadlock, y un exceso de `Done()` podría causar un panic.\n\n## 🧪 Testing con Goroutines\n\nProbar código concurrente puede ser desafiante. Una forma de probar funciones que usan goroutines es capturar su salida:\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"io\"\n    \"os\"\n    \"strings\"\n    \"sync\"\n    \"testing\"\n)\n\nfunc Test_printSomething(t *testing.T) {\n    // Guardamos stdout original\n    originalStdout := os.Stdout\n\n    // Creamos un pipe para capturar la salida\n    r, w, _ := os.Pipe()\n    os.Stdout = w\n\n    // Preparamos el WaitGroup\n    var wg sync.WaitGroup\n    wg.Add(1)\n\n    // Ejecutamos la función como goroutine\n    go printSomething(\u0026wg, \"Mensaje de prueba\")\n\n    // Esperamos a que termine\n    wg.Wait()\n\n    // Cerramos la escritura del pipe\n    w.Close()\n\n    // Leemos lo que se imprimió\n    var buf strings.Builder\n    io.Copy(\u0026buf, r)\n    output := buf.String()\n\n    // Restauramos stdout\n    os.Stdout = originalStdout\n\n    // Verificamos la salida\n    expected := \"Mensaje de prueba\"\n    if !strings.Contains(output, expected) {\n        t.Errorf(\"Esperaba '%s', pero obtuve '%s'\", expected, output)\n    }\n}\n```\n\n## 🔍 Patrones Comunes y Errores a Evitar\n\n### Errores comunes:\n\n1. **Race conditions**: Cuando múltiples goroutines acceden a la misma variable\n\n   ```go\n   // Incorrecto - race condition\n   var counter int\n   go func() { counter++ }()\n   go func() { counter++ }()\n   ```\n\n2. **Deadlocks**: Cuando todas las goroutines están esperando y ninguna puede avanzar\n\n   ```go\n   // Incorrecto - deadlock\n   var wg sync.WaitGroup\n   wg.Add(1)\n   // Olvidamos llamar a wg.Done()\n   wg.Wait() // ¡Se bloqueará para siempre!\n   ```\n\n3. **Goroutines huérfanas**: Goroutines que continúan ejecutándose después de que main termina\n\n### Buenas prácticas:\n\n1. **Usa `sync.Mutex` para proteger datos compartidos**:\n\n   ```go\n   var (\n       counter int\n       mu      sync.Mutex\n   )\n\n   func incrementCounter() {\n       mu.Lock()\n       defer mu.Unlock()\n       counter++\n   }\n   ```\n\n2. **Pasa el WaitGroup por referencia, no por valor**:\n\n   ```go\n   func worker(wg *sync.WaitGroup) { // ✅ Correcto\n       defer wg.Done()\n       // ...\n   }\n   ```\n\n3. **Usa `go vet` y `go run -race` para detectar problemas**:\n   ```bash\n   go vet ./...\n   go run -race main.go\n   ```\n\n## 💪 Desafío: Implementando Concurrencia\n\nModifica este código para que use goroutines y WaitGroups adecuadamente:\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n)\n\nvar msg string\n\nfunc updateMessage(s string) {\n    msg = s\n}\n\nfunc printMessage() {\n    fmt.Println(msg)\n}\n\nfunc main() {\n    msg = \"Hello, world!\"\n\n    updateMessage(\"Hello, universe!\")\n    printMessage()\n\n    updateMessage(\"Hello, cosmos!\")\n    printMessage()\n\n    updateMessage(\"Hello, world!\")\n    printMessage()\n}\n```\n\n### ✅ Solución\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"sync\"\n)\n\nvar msg string\nvar mu sync.Mutex  // Para proteger accesos concurrentes a 'msg'\n\nfunc updateMessage(wg *sync.WaitGroup, s string) {\n    defer wg.Done()\n\n    mu.Lock()\n    msg = s\n    mu.Unlock()\n}\n\nfunc printMessage() {\n    mu.Lock()\n    fmt.Println(msg)\n    mu.Unlock()\n}\n\nfunc main() {\n    var wg sync.WaitGroup\n\n    // Mensaje inicial\n    msg = \"Hello, world!\"\n\n    // Primer cambio\n    wg.Add(1)\n    go updateMessage(\u0026wg, \"Hello, universe!\")\n    wg.Wait()\n    printMessage()\n\n    // Segundo cambio\n    wg.Add(1)\n    go updateMessage(\u0026wg, \"Hello, cosmos!\")\n    wg.Wait()\n    printMessage()\n\n    // Tercer cambio\n    wg.Add(1)\n    go updateMessage(\u0026wg, \"Hello, world!\")\n    wg.Wait()\n    printMessage()\n}\n```\n\n### 🧪 Test para la solución\n\n```go\npackage main\n\nimport (\n    \"bytes\"\n    \"io\"\n    \"os\"\n    \"strings\"\n    \"sync\"\n    \"testing\"\n)\n\nfunc Test_updateMessage(t *testing.T) {\n    var wg sync.WaitGroup\n\n    // Estado inicial\n    oldMsg := msg\n    newMsg := \"Test message\"\n\n    // Actualizamos el mensaje\n    wg.Add(1)\n    go updateMessage(\u0026wg, newMsg)\n    wg.Wait()\n\n    if msg != newMsg {\n        t.Errorf(\"updateMessage() = %v, quería %v\", msg, newMsg)\n    }\n\n    // Restauramos el estado\n    msg = oldMsg\n}\n\nfunc Test_printMessage(t *testing.T) {\n    // Guardamos stdout original\n    oldStdout := os.Stdout\n    r, w, _ := os.Pipe()\n    os.Stdout = w\n\n    // Establecemos un mensaje conocido\n    testMsg := \"Test output message\"\n    msg = testMsg\n\n    // Llamamos a la función\n    printMessage()\n\n    // Restauramos stdout\n    w.Close()\n    os.Stdout = oldStdout\n\n    // Leemos la salida\n    var out bytes.Buffer\n    io.Copy(\u0026out, r)\n\n    // Verificamos\n    if !strings.Contains(out.String(), testMsg) {\n        t.Errorf(\"printMessage() = %v, quería %v\", out.String(), testMsg)\n    }\n}\n\nfunc Test_fullProgram(t *testing.T) {\n    // Guardamos stdout\n    oldStdout := os.Stdout\n    r, w, _ := os.Pipe()\n    os.Stdout = w\n\n    // Ejecutamos el programa\n    main()\n\n    // Restauramos stdout\n    w.Close()\n    os.Stdout = oldStdout\n\n    // Leemos la salida\n    var out bytes.Buffer\n    io.Copy(\u0026out, r)\n    output := out.String()\n\n    // Verificamos las tres líneas\n    expected := []string{\n        \"Hello, universe!\",\n        \"Hello, cosmos!\",\n        \"Hello, world!\",\n    }\n\n    for _, exp := range expected {\n        if !strings.Contains(output, exp) {\n            t.Errorf(\"main() debería imprimir '%s'\", exp)\n        }\n    }\n}\n```\n\n# 🔄 Race Conditions, Mutexes y Channels en Go\n\n## 🚫 Race Conditions: El Gran Problema\n\nUna **race condition** ocurre cuando dos o más goroutines acceden a la misma memoria simultáneamente y al menos una está escribiendo. Esto causa comportamientos impredecibles y errores difíciles de detectar.\n\n\u003e 💡 **Analogía**: Imagina dos personas escribiendo en la misma línea de un documento al mismo tiempo. El resultado será una mezcla confusa de ambas escrituras.\n\nVeamos un ejemplo clásico de race condition:\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"sync\"\n)\n\nfunc main() {\n    counter := 0\n    var wg sync.WaitGroup\n\n    // Lanzamos 1000 goroutines que incrementan el contador\n    for i := 0; i \u003c 1000; i++ {\n        wg.Add(1)\n        go func() {\n            defer wg.Done()\n            counter++ // ⚠️ RACE CONDITION: múltiples goroutines modifican 'counter'\n        }()\n    }\n\n    wg.Wait()\n    fmt.Println(\"Valor final:\", counter) // Casi nunca será 1000\n}\n```\n\nSi ejecutas este código varias veces, obtendrás diferentes resultados. Esto ocurre porque `counter++` no es una operación atómica, sino que implica:\n\n1. Leer el valor actual de counter\n2. Incrementarlo en 1\n3. Escribir el nuevo valor en counter\n\nCuando múltiples goroutines ejecutan estos pasos simultáneamente, pueden pisar los cambios de las demás.\n\n## 🔒 Mutexes: Protegiendo Recursos Compartidos\n\n`Mutex` significa \"Mutual Exclusion\" (Exclusión Mutua). Es una estructura que permite que **solo una goroutine acceda a un código o recurso a la vez**.\n\n### Cómo funciona un Mutex\n\nUn mutex tiene dos operaciones principales:\n\n- **Lock()**: Adquiere el bloqueo (si otra goroutine ya lo tiene, espera)\n- **Unlock()**: Libera el bloqueo para que otras goroutines puedan adquirirlo\n\nCorrigiendo el ejemplo anterior:\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"sync\"\n)\n\nfunc main() {\n    counter := 0\n    var wg sync.WaitGroup\n    var mu sync.Mutex // 🔒 Creamos un mutex\n\n    for i := 0; i \u003c 1000; i++ {\n        wg.Add(1)\n        go func() {\n            defer wg.Done()\n\n            mu.Lock()   // 🔒 Bloqueamos - solo esta goroutine puede acceder\n            counter++\n            mu.Unlock() // 🔓 Liberamos para que otras goroutines puedan acceder\n        }()\n    }\n\n    wg.Wait()\n    fmt.Println(\"Valor final:\", counter) // Ahora siempre será 1000\n}\n```\n\n### Patrón común con Mutex\n\nUn patrón muy común es usar `defer` con `Unlock()`:\n\n```go\nmu.Lock()\ndefer mu.Unlock()\n// código que accede a recursos compartidos\n```\n\nEsto garantiza que el mutex se libere incluso si ocurre un panic o return temprano.\n\n## 📝 Ejemplo Práctico con Mutex\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"sync\"\n)\n\nvar msg string\nvar wg sync.WaitGroup\n\nfunc updateMessage(s string, mu *sync.Mutex) {\n    defer wg.Done()\n\n    mu.Lock()         // 🔒 Bloqueamos acceso exclusivo a msg\n    msg = s           // Modificamos la variable compartida\n    mu.Unlock()       // 🔓 Liberamos el acceso\n}\n\nfunc main() {\n    msg = \"Hello, world!\"\n    var mu sync.Mutex\n\n    wg.Add(2)\n    go updateMessage(\"Hello, universe!\", \u0026mu)\n    go updateMessage(\"Hello, cosmos!\", \u0026mu)\n    wg.Wait()\n\n    // El resultado será uno de los dos mensajes, pero sin race condition\n    fmt.Println(msg)\n}\n```\n\n### 🔍 Detectando Race Conditions\n\nGo incluye un detector de race conditions integrado. Para usarlo:\n\n```bash\n# Al ejecutar un programa\ngo run -race tuarchivo.go\n\n# Al ejecutar tests\ngo test -race ./...\n```\n\nEl detector agregará instrumentación al código que identifica posibles race conditions durante la ejecución.\n\n## 📊 Un Ejemplo Más Complejo: Sistema de Ingresos\n\nVeamos un ejemplo que simula ingresos semanales desde diferentes fuentes:\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"sync\"\n)\n\nvar wg sync.WaitGroup\n\ntype Income struct {\n    Source string\n    Amount int\n}\n\nfunc main() {\n    // Variable para el saldo bancario\n    var bankBalance int\n    var balanceMutex sync.Mutex\n\n    // Mostramos el saldo inicial\n    fmt.Printf(\"Starting bank balance: $%d\\n\", bankBalance)\n\n    // Definimos fuentes de ingreso semanales\n    incomes := []Income {\n        {Source: \"Main job\", Amount: 500},\n        {Source: \"Side hustle\", Amount: 200},\n        {Source: \"Freelance project\", Amount: 50},\n        {Source: \"Selling stuff online\", Amount: 100},\n    }\n    wg.Add(len(incomes))\n\n    // Simulamos 52 semanas de ingresos para cada fuente\n    for i, income := range incomes {\n        go func(i int, income Income) {\n            defer wg.Done()\n\n            for week := 1; week \u003c= 52; week++ {\n                balanceMutex.Lock()\n                // Operación crítica: actualizar el saldo\n                temp := bankBalance\n                temp += income.Amount\n                bankBalance = temp\n                balanceMutex.Unlock()\n\n                fmt.Printf(\"Week %d: +$%d from %s\\n\",\n                           week, income.Amount, income.Source)\n            }\n        }(i, income)  // Pasamos los valores por copia, no por referencia\n    }\n\n    wg.Wait()\n\n    // Mostramos el saldo final\n    fmt.Printf(\"Final bank balance: $%d\\n\", bankBalance)\n}\n```\n\n\u003e ⚠️ **Nota**: Es muy importante pasar `i` e `income` como parámetros a la goroutine y no usar directamente las variables del bucle, ya que estas cambiarán mientras la goroutine se ejecuta.\n\n## 📬 Channels: Comunicación Entre Goroutines\n\nLos **channels** son el mecanismo de Go para comunicar goroutines entre sí. Mientras que los mutexes protegen el acceso a la memoria compartida, los channels siguen la filosofía de Go:\n\n\u003e \"No comuniques compartiendo memoria; comparte memoria comunicándote.\"\n\n### Características principales de los channels:\n\n- Son tipos de datos tipados (solo transmiten un tipo específico)\n- Son thread-safe (no necesitas mutex para usarlos)\n- Pueden ser bufferados o no bufferados\n- Pueden bloquearse hasta que otra goroutine lea/escriba\n\n### Creación y uso básico de channels:\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"time\"\n)\n\nfunc main() {\n    // Creamos un channel de enteros\n    ch := make(chan int)\n\n    // Goroutine que envía datos\n    go func() {\n        fmt.Println(\"Goroutine: Enviando datos...\")\n        ch \u003c- 42 // Enviamos el valor 42 al channel\n    }()\n\n    // Leemos del channel en la goroutine principal\n    fmt.Println(\"Main: Esperando datos...\")\n    valor := \u003c-ch // Esta operación bloquea hasta recibir un valor\n    fmt.Println(\"Main: Recibido\", valor)\n}\n```\n\n### Channels bufferados vs no bufferados\n\n- **No bufferados**: Sincronizan las goroutines que envían y reciben (el envío bloquea hasta que alguien recibe)\n- **Bufferados**: Pueden almacenar un número limitado de valores sin bloquear al remitente\n\n```go\n// Channel no bufferado (capacidad 0)\nch1 := make(chan int)\n\n// Channel bufferado (capacidad 5)\nch2 := make(chan int, 5)\n```\n\n### Ejemplo: Productor-Consumidor con channels\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"time\"\n)\n\nfunc productor(ch chan\u003c- int) {\n    for i := 0; i \u003c 5; i++ {\n        fmt.Printf(\"Productor: enviando %d\\n\", i)\n        ch \u003c- i          // Enviar dato al channel\n        time.Sleep(100 * time.Millisecond)\n    }\n    close(ch)  // Importante: cerrar el channel cuando terminemos\n}\n\nfunc consumidor(ch \u003c-chan int, done chan\u003c- bool) {\n    for valor := range ch {  // range con channel itera hasta que se cierre\n        fmt.Printf(\"Consumidor: recibido %d\\n\", valor)\n    }\n    fmt.Println(\"Consumidor: channel cerrado, terminando\")\n    done \u003c- true\n}\n\nfunc main() {\n    ch := make(chan int)\n    done := make(chan bool)\n\n    go productor(ch)\n    go consumidor(ch, done)\n\n    \u003c-done  // Esperar a que el consumidor termine\n    fmt.Println(\"Programa finalizado\")\n}\n```\n\n## 🤔 ¿Cuándo usar Mutex y cuándo usar Channels?\n\n### 👉 Usa Mutexes cuando:\n\n- Necesites proteger datos compartidos entre goroutines\n- Tengas operaciones atómicas simple sobre variables\n- Quieras implementar tu propio mecanismo de sincronización\n\n### 👉 Usa Channels cuando:\n\n- Necesites transferir propiedad de datos entre goroutines\n- Quieras distribuir unidades de trabajo entre worker pools\n- Necesites comunicar resultados a través de goroutines\n- Quieras implementar el patrón productor-consumidor\n\n## 🧪 Testing de Código Concurrente\n\nPara probar código concurrente con race conditions:\n\n```go\npackage main\n\nimport (\n    \"strings\"\n    \"testing\"\n    \"os\"\n    \"io\"\n)\n\nfunc Test_main(t *testing.T) {\n    // Capturamos la salida estándar\n    stdOut := os.Stdout\n    r, w, _ := os.Pipe()\n    os.Stdout = w\n\n    // Ejecutamos la función principal\n    main()\n\n    // Restauramos la salida y leemos el resultado\n    w.Close()\n    result, _ := io.ReadAll(r)\n    output := string(result)\n    os.Stdout = stdOut\n\n    // Verificamos los resultados\n    if !strings.Contains(output, \"Starting bank balance: $0\") {\n        t.Errorf(\"Expected 'Starting bank balance: $0' but got %s\", output)\n    }\n\n    // Importante: ejecutar este test con go test -race\n    // para detectar posibles race conditions\n}\n```\n\n## 💡 Consejos Prácticos\n\n1. **Usa `-race` regularmente** para detectar race conditions\n2. **Minimiza el alcance del bloqueo** - mantén los mutex bloqueados el menor tiempo posible\n3. **Considera RWMutex** cuando tengas muchas lecturas y pocas escrituras\n4. **Cierra los channels** cuando no enviarás más datos\n5. **Evita el uso de variables globales** - pasa los datos necesarios como parámetros\n\n```go\n// Mejor que bloquear toda la función:\nmu.Lock()\ndefer mu.Unlock()\n// toda la función bloqueada...\n\n// Es más eficiente:\n// código no crítico aquí...\nmu.Lock()\n// Solo el código crítico aquí\nmu.Unlock()\n// más código no crítico...\n```\n\n# 🍕 El Problema del Productor-Consumidor (Producer-Consumer)\n\n## 🧩 ¿Qué es este problema?\n\nEl **problema del productor-consumidor** (también conocido como **bounded-buffer problem**) es uno de los clásicos desafíos de sincronización en programación concurrente. Consiste en coordinar dos tipos de procesos:\n\n- **👨‍🍳 Productores**: Generan datos y los colocan en un buffer compartido\n- **🧑‍🤝‍🧑 Consumidores**: Toman datos del buffer compartido y los procesan\n- **🗄️ Buffer**: Espacio limitado donde se almacenan temporalmente los datos\n\n\u003e 💡 El desafío real es coordinar estos procesos para evitar **race conditions**, asegurar que el buffer no se desborde, y garantizar que los consumidores no intenten tomar datos de un buffer vacío.\n\n## 🍕 La Analogía de la Pizzería\n\nImaginemos una pizzería donde:\n\n- 👨‍🍳 **Cocineros** (productores): Preparan pizzas y las colocan en la ventana de servicio\n- 🧑‍💼 **Camareros** (consumidores): Toman las pizzas de la ventana y las entregan a los clientes\n- 🪟 **Ventana de servicio** (buffer): Espacio limitado donde se colocan las pizzas listas\n\nSi los cocineros producen pizzas más rápido de lo que los camareros pueden entregarlas, la ventana de servicio se llena. Si no hay pizzas listas, los camareros deben esperar.\n\n## 💻 Implementación en Go con Channels\n\nGo resuelve este problema elegantemente mediante **channels**, que funcionan como un buffer sincronizado entre goroutines:\n\n```go\n// Creando un channel bufferado (tamaño 5)\npizzaBuffer := make(chan Pizza, 5)\n\n// Productor: cocina pizzas y las coloca en el buffer\ngo func() {\n    for {\n        pizza := prepararPizza()\n        pizzaBuffer \u003c- pizza  // Se bloquea si el buffer está lleno\n    }\n}()\n\n// Consumidor: toma pizzas del buffer y las entrega\ngo func() {\n    for {\n        pizza := \u003c-pizzaBuffer  // Se bloquea si el buffer está vacío\n        entregarPizza(pizza)\n    }\n}()\n```\n\n## 🚀 Ejemplo Completo: Pizzería Concurrente\n\nVeamos una implementación completa de una pizzería usando concurrencia en Go:\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"math/rand\"\n    \"sync\"\n    \"time\"\n)\n\n// 🍕 Representa un pedido de pizza\ntype PizzaOrder struct {\n    orderNumber int\n    pizzaName   string\n    customer    string\n    ready       bool\n    success     bool\n}\n\n// 👨‍🍳 Productor: La cocina que prepara pizzas\nfunc cocinero(orders chan\u003c- PizzaOrder, wg *sync.WaitGroup) {\n    defer wg.Done()\n\n    pizzaTypes := []string{\"Margherita\", \"Pepperoni\", \"Vegetariana\", \"Hawaiana\", \"Cuatro Quesos\"}\n    customers := []string{\"Carlos\", \"Ana\", \"Luis\", \"Elena\", \"Miguel\"}\n\n    // Preparamos 10 pizzas\n    for i := 1; i \u003c= 10; i++ {\n        // Simulamos tiempo de preparación\n        time.Sleep(time.Millisecond * time.Duration(rand.Intn(500)+300))\n\n        // Creamos el pedido\n        success := rand.Float32() \u003e 0.2  // 20% de probabilidad de fallar\n\n        order := PizzaOrder{\n            orderNumber: i,\n            pizzaName:   pizzaTypes[rand.Intn(len(pizzaTypes))],\n            customer:    customers[rand.Intn(len(customers))],\n            ready:       true,\n            success:     success,\n        }\n\n        fmt.Printf(\"👨‍🍳 Cocinero: Preparando pizza %s para %s (Orden #%d)\\n\",\n                   order.pizzaName, order.customer, order.orderNumber)\n\n        // Enviamos la pizza al buffer (ventana de servicio)\n        orders \u003c- order\n\n        if !success {\n            fmt.Printf(\"⚠️ Cocinero: ¡Ups! La pizza %s para %s salió mal\\n\",\n                      order.pizzaName, order.customer)\n        } else {\n            fmt.Printf(\"✅ Cocinero: ¡Pizza %s para %s lista!\\n\",\n                      order.pizzaName, order.customer)\n        }\n    }\n\n    fmt.Println(\"👨‍🍳 Cocinero: ¡Terminé todos los pedidos por hoy!\")\n    close(orders) // Cerramos el canal cuando no hay más pedidos\n}\n\n// 🧑‍💼 Consumidor: El camarero que entrega las pizzas\nfunc camarero(orders \u003c-chan PizzaOrder, wg *sync.WaitGroup) {\n    defer wg.Done()\n\n    // Atendemos pedidos hasta que la cocina cierre\n    for order := range orders {\n        // Simulamos tiempo de entrega\n        time.Sleep(time.Millisecond * time.Duration(rand.Intn(300)+200))\n\n        if order.success {\n            fmt.Printf(\"🧑‍💼 Camarero: Entregando pizza %s a %s (Orden #%d) ✅\\n\",\n                      order.pizzaName, order.customer, order.orderNumber)\n        } else {\n            fmt.Printf(\"🧑‍💼 Camarero: Disculpándose con %s por la pizza %s fallida (Orden #%d) ❌\\n\",\n                      order.customer, order.pizzaName, order.orderNumber)\n        }\n    }\n\n    fmt.Println(\"🧑‍💼 Camarero: No hay más pedidos, me voy a casa\")\n}\n\nfunc main() {\n    // Semilla para números aleatorios\n    rand.Seed(time.Now().UnixNano())\n\n    fmt.Println(\"🍕 ¡La pizzería está abierta! 🍕\")\n\n    // Creamos un canal bufferado (nuestra ventana de servicio)\n    // Puede contener hasta 3 pizzas a la vez\n    pizzaBuffer := make(chan PizzaOrder, 3)\n\n    var wg sync.WaitGroup\n    wg.Add(2) // Un cocinero y un camarero\n\n    // Iniciamos el cocinero (productor)\n    go cocinero(pizzaBuffer, \u0026wg)\n\n    // Iniciamos el camarero (consumidor)\n    go camarero(pizzaBuffer, \u0026wg)\n\n    // Esperamos a que terminen sus tareas\n    wg.Wait()\n\n    fmt.Println(\"🏁 ¡La pizzería ha cerrado por hoy! 🏁\")\n}\n```\n\n## 🔄 ¿Qué está pasando aquí?\n\n1. **Creamos un buffer limitado** (el channel `pizzaBuffer`) con capacidad para 3 pizzas\n2. **El cocinero (productor)** genera pizzas y las coloca en el buffer:\n   - Si el buffer está lleno, el cocinero se bloquea hasta que haya espacio\n   - Cuando termina todos los pedidos, cierra el channel\n3. **El camarero (consumidor)** toma pizzas del buffer y las entrega:\n   - Si el buffer está vacío, el camarero se bloquea hasta que haya una pizza\n   - El range loop termina automáticamente cuando el channel se cierra\n\n## 💪 Ventajas de usar Channels para este problema\n\n- **Sincronización integrada**: Los channels manejan automáticamente la sincronización\n- **Bloqueo natural**: El productor se bloquea cuando el buffer está lleno, y el consumidor cuando está vacío\n- **Comunicación clara**: El patrón de paso de mensajes hace el código más fácil de entender\n- **Cierre elegante**: Usando `close(channel)` podemos indicar \"fin de la producción\"\n\n## 🌟 Variantes avanzadas\n\nPuedes expandir este patrón para casos más complejos:\n\n### Múltiples productores y consumidores\n\n```go\n// Lanzamos varios cocineros\nfor i := 1; i \u003c= 3; i++ {\n    wg.Add(1)\n    go cocinero(i, pizzaBuffer, \u0026wg)\n}\n\n// Lanzamos varios camareros\nfor i := 1; i \u003c= 2; i++ {\n    wg.Add(1)\n    go camarero(i, pizzaBuffer, \u0026wg)\n}\n```\n\n### Select para manejar múltiples canales\n\n```go\nselect {\n    case normalPizza := \u003c-normalOrders:\n        // Procesar pedido normal\n    case urgentPizza := \u003c-urgentOrders:\n        // Procesar pedido urgente\n    case \u003c-time.After(30 * time.Second):\n        // Timeout si no hay pedidos por 30 segundos\n}\n```\n\n## 🧠 Conceptos clave a recordar\n\n- Un **buffer limitado** previene que el productor sobrecargue al consumidor\n- Los **channels en Go** son una implementación natural del buffer para este problema\n- El **bloqueo** ocurre automáticamente cuando un channel está lleno o vacío\n- **Cerrar un channel** es la forma de indicar \"no más producción\"\n- Un **range sobre un channel** itera hasta que el channel se cierra\n\nEste patrón es fundamental en sistemas concurrentes y lo encontrarás en muchas aplicaciones del mundo real, desde procesamiento de solicitudes web hasta sistemas de streaming de datos.\n\n# 🍽️ El Problema de los Filósofos Comensales (Dining Philosophers)\n\n## 🧠 ¿Qué es este problema?\n\nEl **Problema de los Filósofos Comensales** es uno de los ejemplos clásicos en la teoría de concurrencia, introducido por el científico computacional Edsger W. Dijkstra en 1965. Este problema ilustra los desafíos de asignación de recursos y prevención de interbloqueos (deadlocks).\n\n![Dining Philosophers](./img/theDiningPhilosophers.png)\n\n\u003e 💭 **La metáfora**: Cinco filósofos están sentados alrededor de una mesa redonda. Cada uno tiene un plato de espaguetis y necesita dos tenedores para comer. Sin embargo, solo hay cinco tenedores en total, uno entre cada par de filósofos.\n\n## 🚫 El desafío: Evitar el deadlock\n\nEl problema presenta un clásico escenario de interbloqueo:\n\n1. Cada filósofo necesita DOS recursos (tenedores) para realizar su actividad (comer)\n2. Si todos los filósofos toman su tenedor izquierdo simultáneamente, ninguno podrá tomar el derecho\n3. Resultado: todos quedan esperando indefinidamente → **deadlock** 🔒\n\n## 🧩 La solución de Dijkstra en Go\n\nLa solución implementada usa una técnica llamada \"rompimiento de simetría\": algunos filósofos toman primero el tenedor de menor número, mientras que otros toman primero el de mayor número.\n\n```go\n// Si el tenedor izquierdo tiene un número mayor, tomar primero el derecho\nif philosopher.leftFork \u003e philosopher.rightFork {\n    forks[philosopher.rightFork].Lock()\n    // ...\n    forks[philosopher.leftFork].Lock()\n} else { // De lo contrario, tomar primero el izquierdo\n    forks[philosopher.leftFork].Lock()\n    // ...\n    forks[philosopher.rightFork].Lock()\n}\n```\n\n## 📝 Explicación completa del código\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"sync\"\n    \"time\"\n)\n\n// Estructura para representar a un filósofo\ntype Philosopher struct {\n    name      string\n    rightFork int\n    leftFork  int\n}\n\n// Lista de filósofos con sus tenedores asignados\nvar philosophers = []Philosopher{\n    {name: \"Kant\", leftFork: 4, rightFork: 0},\n    {name: \"Hume\", leftFork: 0, rightFork: 1},\n    {name: \"Descartes\", leftFork: 1, rightFork: 2},\n    {name: \"Nietzsche\", leftFork: 2, rightFork: 3},\n    {name: \"Wittgenstein\", leftFork: 3, rightFork: 4},\n}\n\n// Variables para configurar el comportamiento\nvar hunger = 3                    // Cuántas veces comerá cada filósofo\nvar eatTime = 1 * time.Second     // Tiempo dedicado a comer\nvar thinkTime = 3 * time.Second   // Tiempo dedicado a pensar\nvar sleepTime = 1 * time.Second   // Pausa inicial para mejor visualización\n\nvar orderMutex sync.Mutex         // Mutex para proteger la lista orderFinished\nvar orderFinished []string        // Registro del orden en que terminan de comer\n\nfunc main() {\n    // Mensaje de bienvenida\n    fmt.Println(\"El Problema de los Filósofos Comensales\")\n    fmt.Println(\"--------------------------------------\")\n    fmt.Println(\"La mesa está vacía.\")\n\n    time.Sleep(sleepTime)\n\n    // Comenzamos la cena\n    dine()\n\n    // Mensaje de finalización\n    fmt.Println(\"La mesa está vacía.\")\n    fmt.Println(\"--------------------------------------\")\n\n    // Podríamos mostrar el orden en que terminaron\n    fmt.Println(\"Orden de finalización:\", orderFinished)\n}\n\nfunc dine() {\n    // WaitGroup para esperar a que todos terminen de comer\n    wg := \u0026sync.WaitGroup{}\n    wg.Add(len(philosophers))\n\n    // WaitGroup para que todos estén sentados antes de comenzar\n    seated := \u0026sync.WaitGroup{}\n    seated.Add(len(philosophers))\n\n    // Creamos los mutex para cada tenedor\n    var forks = make(map[int]*sync.Mutex)\n    for i := 0; i \u003c len(philosophers); i++ {\n        forks[i] = \u0026sync.Mutex{}\n    }\n\n    // Lanzamos una goroutine para cada filósofo\n    for i := 0; i \u003c len(philosophers); i++ {\n        go diningProblem(philosophers[i], wg, forks, seated)\n    }\n\n    // Esperamos a que todos terminen\n    wg.Wait()\n}\n\nfunc diningProblem(philosopher Philosopher, wg *sync.WaitGroup, forks map[int]*sync.Mutex, seated *sync.WaitGroup) {\n    defer wg.Done() // Señalamos que el filósofo terminó cuando salga la función\n\n    // El filósofo se sienta\n    fmt.Printf(\"%s se sienta a la mesa.\\n\", philosopher.name)\n    seated.Done()\n\n    // Esperamos a que todos estén sentados\n    seated.Wait()\n\n    // Ciclo de comer (según el hambre configurada)\n    for i := hunger; i \u003e 0; i-- {\n        // 🔑 SOLUCIÓN AL DEADLOCK: Romper la simetría en cómo toman los tenedores\n        if philosopher.leftFork \u003e philosopher.rightFork {\n            // Algunos filósofos toman primero el tenedor derecho\n            forks[philosopher.rightFork].Lock()\n            fmt.Printf(\"%s tiene el tenedor derecho.\\n\", philosopher.name)\n            forks[philosopher.leftFork].Lock()\n            fmt.Printf(\"%s tiene el tenedor izquierdo.\\n\", philosopher.name)\n        } else {\n            // Otros toman primero el izquierdo\n            forks[philosopher.leftFork].Lock()\n            fmt.Printf(\"%s tiene el tenedor izquierdo.\\n\", philosopher.name)\n            forks[philosopher.rightFork].Lock()\n            fmt.Printf(\"%s tiene el tenedor derecho.\\n\", philosopher.name)\n        }\n\n        // El filósofo come\n        fmt.Printf(\"🍝 %s está comiendo.\\n\", philosopher.name)\n        time.Sleep(eatTime)\n\n        // El filósofo piensa\n        fmt.Printf(\"🤔 %s está pensando.\\n\", philosopher.name)\n        time.Sleep(thinkTime)\n\n        // Suelta los tenedores\n        forks[philosopher.leftFork].Unlock()\n        forks[philosopher.rightFork].Unlock()\n        fmt.Printf(\"🍴 %s dejó los tenedores.\\n\", philosopher.name)\n    }\n\n    // El filósofo termina y se va\n    fmt.Printf(\"✅ %s terminó de comer.\\n\", philosopher.name)\n    fmt.Printf(\"👋 %s se retira de la mesa.\\n\", philosopher.name)\n\n    // Registramos el orden de finalización (protegido por mutex)\n    orderMutex.Lock()\n    orderFinished = append(orderFinished, philosopher.name)\n    orderMutex.Unlock()\n}\n```\n\n## 🔍 ¿Por qué esta solución funciona?\n\nLa solución implementada evita el deadlock mediante tres estrategias clave:\n\n1. **Rompimiento de simetría** 🔄: Al hacer que algunos filósofos tomen los tenedores en orden diferente (izquierdo-derecho vs derecho-izquierdo), se rompe la condición circular que causa el deadlock.\n\n2. **Mutex para cada tenedor** 🔒: Cada tenedor está modelado como un mutex, lo que garantiza acceso exclusivo.\n\n3. **Coordinación para sentarse** ⏱️: Usamos `seated` WaitGroup para asegurar que todos los filósofos estén listos antes de comenzar a competir por los tenedores.\n\n## 💡 Conceptos ilustrados por este problema\n\n1. **Deadlock (interbloqueo)**: Situación donde un grupo de procesos espera indefinidamente por recursos que otro proceso del mismo grupo posee.\n\n2. **Livelock**: Situación donde los procesos cambian de estado continuamente pero no avanzan (como dos personas en un pasillo tratando de ceder el paso).\n\n3. **Inanición (starvation)**: Cuando un proceso nunca recibe el recurso que necesita porque otros procesos siempre tienen prioridad.\n\n4. **Exclusión mutua**: Garantía de que sólo un proceso puede utilizar un recurso a la vez.\n\n## 🧪 Testing el problema\n\nEl código de prueba verifica que la solución funcione correctamente:\n\n```go\nfunc Test_dine(t *testing.T) {\n    // Aceleramos la ejecución para las pruebas\n    eatTime = 0 * time.Second\n    sleepTime = 0 * time.Second\n    thinkTime = 0 * time.Second\n\n    // Ejecutamos 10 veces para asegurarnos de que no hay deadlock\n    for i := 0; i \u003c 10; i++ {\n        orderFinished = []string{}\n        dine()\n        if len(orderFinished) != 5 {\n            t.Errorf(\"Esperaba que 5 filósofos terminaran de comer, obtuve %d\", len(orderFinished))\n        }\n    }\n}\n```\n\nTambién se prueba con diferentes tiempos para asegurar robustez:\n\n```go\nfunc Test_dineWithVaryingDelays(t *testing.T) {\n    var theTest = []struct{\n        name string\n        delay time.Duration\n    } {\n        {\"rápido\", 0 * time.Second},\n        {\"lento\", 1 * time.Second},\n        {\"muy lento\", 2 * time.Second},\n    }\n\n    // Prueba con diferentes velocidades\n    for _, test := range theTest {\n        eatTime = test.delay\n        sleepTime = test.delay\n        thinkTime = test.delay\n\n        for i := 0; i \u003c 10; i++ {\n            orderFinished = []string{}\n            dine()\n            if len(orderFinished) != 5 {\n                t.Errorf(\"Esperaba que 5 filósofos terminaran de comer, obtuve %d\", len(orderFinished))\n            }\n        }\n    }\n}\n```\n\n## 🌟 Otras soluciones al problema\n\nExisten diferentes enfoques para resolver este problema:\n\n1. **Camarero**: Introducir un actor centralizado (camarero) que controla quién puede tomar los tenedores.\n\n2. **Jerarquía de recursos**: Numerar todos los recursos y requerir que los procesos los adquieran en orden numérico.\n\n3. **Limitación de comensales**: Permitir solo 4 filósofos a la mesa simultáneamente (evitando la condición de deadlock).\n\n4. **Tenedores compartidos**: Usar un único mutex para controlar simultáneamente ambos tenedores.\n\n## 🎓 ¿Por qué es importante este problema?\n\nEl problema de los Filósofos Comensales es importante porque:\n\n- Modela situaciones reales de sistemas operativos, bases de datos y otros sistemas concurrentes\n- Demuestra las dificultades de la asignación de recursos compartidos\n- Ilustra claramente los peligros del deadlock\n- Proporciona un marco para enseñar y evaluar soluciones de sincronización\n\nEste problema, aunque simple en su concepto, captura la esencia de muchos desafíos que encontramos en sistemas concurrentes modernos, desde servidores web hasta sistemas distribuidos.\n\n# 🍽️ El Problema de los Filósofos Comensales: Soluciones Avanzadas\n\n## 🔄 Recordatorio del Problema\n\nEl problema de los filósofos comensales plantea un escenario donde cinco filósofos se sientan alrededor de una mesa circular. Cada uno necesita dos tenedores para comer, pero hay solo un tenedor entre cada par de filósofos. El desafío es diseñar un algoritmo que permita a todos comer sin caer en un deadlock.\n\n## 1️⃣ Solución con Camarero\n\n### 📝 Concepto\n\nIntroducimos un \"camarero\" como coordinador central que decide quién puede tomar los tenedores. Los filósofos deben \"pedir permiso\" antes de intentar tomar los tenedores.\n\n### 💻 Implementación\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"sync\"\n    \"time\"\n)\n\ntype Philosopher struct {\n    name      string\n    rightFork int\n    leftFork  int\n}\n\n// Estructura para representar al camarero\ntype Waiter struct {\n    sync.Mutex\n}\n\nfunc (w *Waiter) RequestForks(philosopher *Philosopher, forks map[int]*sync.Mutex) bool {\n    w.Lock()\n    defer w.Unlock()\n\n    // El camarero verifica si ambos tenedores están disponibles\n    if !forks[philosopher.leftFork].TryLock() {\n        return false\n    }\n\n    if !forks[philosopher.rightFork].TryLock() {\n        // Si el segundo no está disponible, suelta el primero\n        forks[philosopher.leftFork].Unlock()\n        return false\n    }\n\n    fmt.Printf(\"🧑‍🍳 Camarero: %s puede tomar ambos tenedores.\\n\", philosopher.name)\n    return true\n}\n\nfunc (w *Waiter) ReleaseForks(philosopher *Philosopher, forks map[int]*sync.Mutex) {\n    w.Lock()\n    defer w.Unlock()\n\n    forks[philosopher.leftFork].Unlock()\n    forks[philosopher.rightFork].Unlock()\n    fmt.Printf(\"🧑‍🍳 Camarero: %s ha devuelto los tenedores.\\n\", philosopher.name)\n}\n\nfunc diningWithWaiter() {\n    var wg sync.WaitGroup\n    wg.Add(len(philosophers))\n\n    // Creamos los tenedores\n    var forks = make(map[int]*sync.Mutex)\n    for i := 0; i \u003c len(philosophers); i++ {\n        forks[i] = \u0026sync.Mutex{}\n    }\n\n    // Creamos al camarero\n    waiter := \u0026Waiter{}\n\n    // Lanzamos los filósofos\n    for i := 0; i \u003c len(philosophers); i++ {\n        go func(i int) {\n            defer wg.Done()\n\n            philosopher := philosophers[i]\n            fmt.Printf(\"%s se sienta a la mesa.\\n\", philosopher.name)\n\n            // Cada filósofo intenta comer 'hunger' veces\n            for j := 0; j \u003c hunger; j++ {\n                // El filósofo piensa\n                fmt.Printf(\"🤔 %s está pensando...\\n\", philosopher.name)\n                time.Sleep(thinkTime)\n\n                // Solicita permiso al camarero para tomar los tenedores\n                for {\n                    if waiter.RequestForks(\u0026philosopher, forks) {\n                        break\n                    }\n                    // Si no puede tomar los tenedores, espera un poco\n                    time.Sleep(100 * time.Millisecond)\n                }\n\n                // Ahora puede comer\n                fmt.Printf(\"🍝 %s está comiendo.\\n\", philosopher.name)\n                time.Sleep(eatTime)\n\n                // Devuelve los tenedores\n                waiter.ReleaseForks(\u0026philosopher, forks)\n            }\n\n            fmt.Printf(\"✅ %s ha terminado y se retira de la mesa.\\n\", philosopher.name)\n        }(i)\n    }\n\n    wg.Wait()\n}\n```\n\n### ✅ Ventajas\n\n- Previene deadlock de forma centralizada\n- Fácil de entender y razonar sobre su corrección\n- Permite priorizar a ciertos filósofos si es necesario\n\n### ❌ Desventajas\n\n- El camarero puede convertirse en un cuello de botella\n- Reduce el paralelismo al centralizar las decisiones\n- Mayor complejidad de implementación\n\n## 2️⃣ Solución con Jerarquía de Recursos\n\n### 📝 Concepto\n\nAsignamos un número único a cada tenedor y requerimos que los filósofos tomen los tenedores en orden numérico ascendente. Esta estrategia es una generalización de la solución en el código original.\n\n### 💻 Implementación más explícita\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"sort\"\n    \"sync\"\n    \"time\"\n)\n\ntype Philosopher struct {\n    name      string\n    forkIDs   []int    // IDs ordenados de los tenedores que usa\n}\n\nfunc diningWithHierarchy() {\n    var wg sync.WaitGroup\n    wg.Add(len(philosophers))\n\n    // Creamos los tenedores\n    var forks = make(map[int]*sync.Mutex)\n    for i := 0; i \u003c len(philosophers); i++ {\n        forks[i] = \u0026sync.Mutex{}\n    }\n\n    // Preparamos a los filósofos con sus tenedores en orden\n    hierarchicalPhilosophers := []Philosopher{\n        {name: \"Kant\", forkIDs: []int{0, 4}},\n        {name: \"Hume\", forkIDs: []int{0, 1}},\n        {name: \"Descartes\", forkIDs: []int{1, 2}},\n        {name: \"Nietzsche\", forkIDs: []int{2, 3}},\n        {name: \"Wittgenstein\", forkIDs: []int{3, 4}},\n    }\n\n    // Ordenamos los tenedores para cada filósofo\n    for i := range hierarchicalPhilosophers {\n        sort.Ints(hierarchicalPhilosophers[i].forkIDs)\n    }\n\n    // Lanzamos los filósofos\n    for i := 0; i \u003c len(hierarchicalPhilosophers); i++ {\n        go func(i int) {\n            defer wg.Done()\n\n            philosopher := hierarchicalPhilosophers[i]\n            fmt.Printf(\"%s se sienta a la mesa.\\n\", philosopher.name)\n\n            // Cada filósofo intenta comer 'hunger' veces\n            for j := 0; j \u003c hunger; j++ {\n                // El filósofo piensa\n                fmt.Printf(\"🤔 %s está pensando...\\n\", philosopher.name)\n                time.Sleep(thinkTime)\n\n                // Toma el tenedor con el número menor primero\n                fmt.Printf(\"%s intenta tomar el tenedor %d.\\n\",\n                          philosopher.name, philosopher.forkIDs[0])\n                forks[philosopher.forkIDs[0]].Lock()\n                fmt.Printf(\"%s tiene el tenedor %d.\\n\",\n                          philosopher.name, philosopher.forkIDs[0])\n\n                // Luego toma el tenedor con el número mayor\n                fmt.Printf(\"%s intenta tomar el tenedor %d.\\n\",\n                          philosopher.name, philosopher.forkIDs[1])\n                forks[philosopher.forkIDs[1]].Lock()\n                fmt.Printf(\"%s tiene el tenedor %d.\\n\",\n                          philosopher.name, philosopher.forkIDs[1])\n\n                // Ahora puede comer\n                fmt.Printf(\"🍝 %s está comiendo.\\n\", philosopher.name)\n                time.Sleep(eatTime)\n\n                // Suelta los tenedores en cualquier orden\n                forks[philosopher.forkIDs[0]].Unlock()\n                forks[philosopher.forkIDs[1]].Unlock()\n                fmt.Printf(\"🍴 %s ha soltado ambos tenedores.\\n\", philosopher.name)\n            }\n\n            fmt.Printf(\"✅ %s ha terminado y se retira de la mesa.\\n\", philosopher.name)\n        }(i)\n    }\n\n    wg.Wait()\n}\n```\n\n### ✅ Ventajas\n\n- Previene deadlock sin necesidad de un coordinador central\n- Permite mayor paralelismo que la solución del camarero\n- Conceptualmente simple\n\n### ❌ Desventajas\n\n- Puede llevar a inanición (starvation) de algunos filósofos\n- Requiere una numeración consistente de todos los recursos\n- Difícil de extender a sistemas distribuidos\n\n## 3️⃣ Solución con Limitación de Comensales\n\n### 📝 Concepto\n\nLimitamos el número de filósofos que pueden intentar comer simultáneamente. Con 5 filósofos y 5 tenedores, si solo permitimos que 4 filósofos intenten comer a la vez, garantizamos que al menos uno podrá obtener ambos tenedores.\n\n### 💻 Implementación\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"sync\"\n    \"time\"\n)\n\nfunc diningWithLimitation() {\n    var wg sync.WaitGroup\n    wg.Add(len(philosophers))\n\n    // Creamos los tenedores\n    var forks = make(map[int]*sync.Mutex)\n    for i := 0; i \u003c len(philosophers); i++ {\n        forks[i] = \u0026sync.Mutex{}\n    }\n\n    // Semáforo que limita el número de filósofos comiendo simultáneamente\n    // Con 5 filósofos, limitamos a 4 para evitar deadlock\n    maxEating := sync.NewCond(\u0026sync.Mutex{})\n    eating := 0\n    maxAllowed := len(philosophers) - 1\n\n    // Lanzamos los filósofos\n    for i := 0; i \u003c len(philosophers); i++ {\n        go func(i int) {\n            defer wg.Done()\n\n            philosopher := philosophers[i]\n            fmt.Printf(\"%s se sienta a la mesa.\\n\", philosopher.name)\n\n            // Cada filósofo intenta comer 'hunger' veces\n            for j := 0; j \u003c hunger; j++ {\n                // El filósofo piensa\n                fmt.Printf(\"🤔 %s está pensando...\\n\", philosopher.name)\n                time.Sleep(thinkTime)\n\n                // Solicita permiso para intentar comer\n                maxEating.L.Lock()\n                for eating \u003e= maxAllowed {\n                    fmt.Printf(\"⌛ %s espera permiso para intentar comer...\\n\", philosopher.name)\n                    maxEating.Wait()\n                }\n                eating++\n                maxEating.L.Unlock()\n\n                // Toma los tenedores en cualquier orden\n                fmt.Printf(\"%s intenta tomar los tenedores.\\n\", philosopher.name)\n                forks[philosopher.leftFork].Lock()\n                forks[philosopher.rightFork].Lock()\n\n                // Ahora puede comer\n                fmt.Printf(\"🍝 %s está comiendo.\\n\", philosopher.name)\n                time.Sleep(eatTime)\n\n                // Suelta los tenedores\n                forks[philosopher.leftFork].Unlock()\n                forks[philosopher.rightFork].Unlock()\n                fmt.Printf(\"🍴 %s ha soltado los tenedores.\\n\", philosopher.name)\n\n                // Notifica que ya no está comiendo\n                maxEating.L.Lock()\n                eating--\n                maxEating.Signal() // Notifica a un filósofo en espera\n                maxEating.L.Unlock()\n            }\n\n            fmt.Printf(\"✅ %s ha terminado y se retira de la mesa.\\n\", philosopher.name)\n        }(i)\n    }\n\n    wg.Wait()\n}\n```\n\n### ✅ Ventajas\n\n- Garantiza que no habrá deadlock\n- Simple de implementar y entender\n- Permite cierta flexibilidad en el acceso a los recursos\n\n### ❌ Desventajas\n\n- Limita el paralelismo potencial\n- Puede llevar a una subutilización de recursos\n- Posible inanición de algunos filósofos\n\n## 4️⃣ Solución con Tenedores Compartidos\n\n### 📝 Concepto\n\nEn lugar de controlar cada tenedor individualmente, controlamos el acceso a ambos tenedores de un filósofo como una unidad atómica usando un único mutex.\n\n### 💻 Implementación\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"sync\"\n    \"time\"\n)\n\nfunc diningWithSharedForks() {\n    var wg sync.WaitGroup\n    wg.Add(len(philosophers))\n\n    // En lugar de mutex individuales, creamos \"pares de tenedores\"\n    // Cada par representa los dos tenedores necesarios para un filósofo\n    var forkPairs = make(map[string]*sync.Mutex)\n    for i := 0; i \u003c len(philosophers); i++ {\n        // Creamos un identificador único para cada par de tenedores\n        pairID := fmt.Sprintf(\"%d-%d\", philosophers[i].leftFork, philosophers[i].rightFork)\n        forkPairs[pairID] = \u0026sync.Mutex{}\n    }\n\n    // Lanzamos los filósofos\n    for i := 0; i \u003c len(philosophers); i++ {\n        go func(i int) {\n            defer wg.Done()\n\n            philosopher := philosophers[i]\n            fmt.Printf(\"%s se sienta a la mesa.\\n\", philosopher.name)\n\n            // Identificador para este par de tenedores\n            pairID := fmt.Sprintf(\"%d-%d\", philosopher.leftFork, philosopher.rightFork)\n\n            // Cada filósofo intenta comer 'hunger' veces\n            for j := 0; j \u003c hunger; j++ {\n                // El filósofo piensa\n                fmt.Printf(\"🤔 %s está pensando...\\n\", philosopher.name)\n                time.Sleep(thinkTime)\n\n                // Intenta tomar ambos tenedores a la vez\n                fmt.Printf(\"%s intenta tomar sus tenedores (par %s).\\n\",\n                          philosopher.name, pairID)\n                forkPairs[pairID].Lock()\n\n                // Si llegamos aquí, tiene ambos tenedores\n                fmt.Printf(\"%s tiene ambos tenedores.\\n\", philosopher.name)\n\n                // Ahora puede comer\n                fmt.Printf(\"🍝 %s está comiendo.\\n\", philosopher.name)\n                time.Sleep(eatTime)\n\n                // Suelta ambos tenedores a la vez\n                forkPairs[pairID].Unlock()\n                fmt.Printf(\"🍴 %s ha soltado sus tenedores.\\n\", philosopher.name)\n            }\n\n            fmt.Printf(\"✅ %s ha terminado y se retira de la mesa.\\n\", philosopher.name)\n        }(i)\n    }\n\n    wg.Wait()\n}\n```\n\n### ✅ Ventajas\n\n- Elimina la posibilidad de deadlock al hacer atómica la adquisición de recursos\n- Simplifica el razonamiento sobre la corrección del algoritmo\n- Evita problemas de adquisición parcial de recursos\n\n### ❌ Desventajas\n\n- Puede reducir el paralelismo al bloquear pares de tenedores juntos\n- No representa fielmente la granularidad del problema original\n- Puede llevar a una subutilización de recursos\n\n## 5️⃣ Solución con Canales (Go-style)\n\n### 📝 Concepto\n\nAprovechamos los canales de Go para modelar los tenedores como recursos que se pasan entre los filósofos.\n\n### 💻 Implementación\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"sync\"\n    \"time\"\n)\n\nfunc diningWithChannels() {\n    var wg sync.WaitGroup\n    wg.Add(len(philosophers))\n\n    // Creamos canales para cada tenedor\n    forks := make([]chan struct{}, len(philosophers))\n    for i := 0; i \u003c len(philosophers); i++ {\n        forks[i] = make(chan struct{}, 1)\n        // Inicialmente cada tenedor está en la mesa\n        forks[i] \u003c- struct{}{}\n    }\n\n    // Función helper para tomar un tenedor\n    takeFork := func(fork chan struct{}, name string, forkID int) {\n        \u003c-fork\n        fmt.Printf(\"%s toma el tenedor %d.\\n\", name, forkID)\n    }\n\n    // Función helper para dejar un tenedor\n    putFork := func(fork chan struct{}, name string, forkID int) {\n        fork \u003c- struct{}{}\n        fmt.Printf(\"%s deja el tenedor %d.\\n\", name, forkID)\n    }\n\n    // Lanzamos los filósofos\n    for i := 0; i \u003c len(philosophers); i++ {\n        go func(i int) {\n            defer wg.Done()\n\n            philosopher := philosophers[i]\n            fmt.Printf(\"%s se sienta a la mesa.\\n\", philosopher.name)\n\n            // Prevenimos deadlock asegurando un orden consistente\n            leftForkID := philosopher.leftFork\n            rightForkID := philosopher.rightFork\n\n            if leftForkID \u003e rightForkID {\n                leftForkID, rightForkID = rightForkID, leftForkID\n            }\n\n            // Cada filósofo intenta comer 'hunger' veces\n            for j := 0; j \u003c hunger; j++ {\n                // El filósofo piensa\n                fmt.Printf(\"🤔 %s está pensando...\\n\", philosopher.name)\n                time.Sleep(thinkTime)\n\n                // Toma los tenedores en orden numérico para evitar deadlock\n                takeFork(forks[leftForkID], philosopher.name, leftForkID)\n                takeFork(forks[rightForkID], philosopher.name, rightForkID)\n\n                // Ahora puede comer\n                fmt.Printf(\"🍝 %s está comiendo.\\n\", philosopher.name)\n                time.Sleep(eatTime)\n\n                // Deja los tenedores\n                putFork(forks[rightForkID], philosopher.name, rightForkID)\n                putFork(forks[leftForkID], philosopher.name, leftForkID)\n            }\n\n            fmt.Printf(\"✅ %s ha terminado y se retira de la mesa.\\n\", philosopher.name)\n        }(i)\n    }\n\n    wg.Wait()\n}\n```\n\n### ✅ Ventajas\n\n- Utiliza el modelo de comunicación nativo de Go\n- Los canales facilitan la visualización del paso de recursos\n- Código expresivo y conciso\n\n### ❌ Desventajas\n\n- Puede ser menos eficiente que una implementación con mutex\n- Requiere cuidadosa consideración del tamaño del buffer del canal\n- El orden de adquisición sigue siendo crucial para evitar deadlocks\n\n## 🔄 Comparación de las Soluciones\n\n| Solución              | Previene Deadlock | Nivel de Paralelismo | Complejidad | Equidad             |\n| --------------------- | ----------------- | -------------------- | ----------- | ------------------- |\n| Original (Dijkstra)   | ✅                | Alto                 | Media       | Media               |\n| Camarero              | ✅                | Bajo-Medio           | Media       | Alta (configurable) |\n| Jerarquía             | ✅                | Alto                 | Baja        | Baja                |\n| Limitación            | ✅                | Medio                | Baja        | Media               |\n| Tenedores Compartidos | ✅                | Medio                | Baja        | Media               |\n| Canales               | ✅                | Alto                 | Media       | Media               |\n\n## 🧪 Probando las Soluciones\n\nPara probar exhaustivamente estas soluciones, podríamos implementar un test que verifica:\n\n```go\npackage main\n\nimport (\n    \"testing\"\n    \"time\"\n)\n\nfunc TestAllSolutions(t *testing.T) {\n    // Configuración rápida para testing\n    originalHunger := hunger\n    originalEatTime := eatTime\n    originalThinkTime := thinkTime\n\n    hunger = 3\n    eatTime = 10 * time.Millisecond\n    thinkTime = 10 * time.Millisecond\n\n    // Restauramos los valores originales al finalizar\n    defer func() {\n        hunger = originalHunger\n        eatTime = originalEatTime\n        thinkTime = originalThinkTime\n    }()\n\n    testCases := []struct{\n        name string\n        solution func()\n    }{\n        {\"Original (Dijkstra)\", dine},\n        {\"Camarero\", diningWithWaiter},\n        {\"Jerarquía\", diningWithHierarchy},\n        {\"Limitación\", diningWithLimitation},\n        {\"Tenedores Compartidos\", diningWithSharedForks},\n        {\"Canales\", diningWithChannels},\n    }\n\n    for _, tc := range testCases {\n        t.Run(tc.name, func(t *testing.T) {\n            // Ejecutamos cada solución varias veces para verificar consistencia\n            for i := 0; i \u003c 5; i++ {\n                orderFinished = nil\n                tc.solution()\n\n                if len(orderFinished) != len(philosophers) {\n                    t.Errorf(\"%s: No todos los filósofos terminaron (iteración %d)\", tc.name, i)\n                }\n            }\n        })\n    }\n}\n```\n\n## 📝 Conclusiones Prácticas\n\nEl problema de los filósofos comensales ilustra desafíos fundamentales en concurrencia:\n\n1. **Evitar deadlocks** es primordial en sistemas concurrentes\n2. **La coordinación** puede lograrse tanto de forma centralizada como descentralizada\n3. **El equilibrio entre paralelismo y control** es una consideración de diseño importante\n\nAl elegir una solución:\n\n- En **sistemas pequeños**, la solución del camarero puede ser la más simple y clara\n- Para **alta disponibilidad**, las soluciones descentralizadas como la jerarquía son preferibles\n- En **Go específicamente**, la solución basada en canales alinea mejor con la filosofía del lenguaje\n- Para **rendimiento óptimo**, la solución original de Dijkstra suele ofrecer el mejor balance\n\nEstos patrones de diseño concurrente aparecen en muchos sistemas modernos:\n\n- Administradores de conexiones de base de datos\n- Sistemas de gestión de recursos en la nube\n- Planificadores de tareas\n- Sistemas de archivos distribuidos\n\nLa comprensión profunda de estas soluciones proporciona una base sólida para diseñar sistemas concurrentes robustos y eficientes.\n\n# 📡 Channels en Go: Comunicación Entre Goroutines\n\n## 🧠 Entendiendo los Channels\n\nLos **channels** son una de las características más poderosas y distintivas de Go. Actúan como conductos tipados que permiten a las goroutines comunicarse entre sí, siguiendo el principio fundamental:\n\n\u003e \"No comuniques compartiendo memoria; comparte memoria comunicándote\"\n\n### 🔍 ¿Qué son exactamente los channels?\n\nUn channel en Go es una estructura de datos tipada que funciona como una tubería por la que pueden fluir valores entre goroutines:\n\n```go\n// Creando un channel básico que transmite enteros\nch := make(chan int)\n\n// Enviando un valor al channel\nch \u003c- 42\n\n// Recibiendo un valor del channel\nvalor := \u003c-ch\n```\n\nLos channels proporcionan:\n\n- **Sincronización**: Coordinan la ejecución entre goroutines\n- **Comunicación**: Permiten el intercambio seguro de datos\n- **Garantías de memoria**: Aseguran la visibilidad de los cambios entre goroutines\n\n## 📊 Tipos de Channels\n\n### 1️⃣ Por capacidad de buffer\n\n#### Unbuffered Channels (Sin buffer)\n\n```go\nch := make(chan string) // Channel sin buffer\n```\n\n- **Comportamiento**: Las operaciones de envío bloquean hasta que otra goroutine recibe el valor\n- **Analogía**: Como pasar un testigo en una carrera de relevos - debes esperar a que alguien tome el testigo\n\n```go\n// Ejemplo de channel sin buffer\nfunc main() {\n    ch := make(chan string)\n\n    go func() {\n        msg := \u003c-ch // Esta goroutine debe ejecutarse primero y esperar\n        fmt.Println(\"Recibido:\", msg)\n    }()\n\n    time.Sleep(time.Second) // Damos tiempo a que la goroutine se inicie\n    ch \u003c- \"Hola mundo\"      // Se bloquea hasta que alguien reciba\n    fmt.Println(\"Mensaje enviado\")\n}\n```\n\n#### Buffered Channels (Con buffer)\n\n```go\nch := make(chan string, 5) // Channel con buffer de tamaño 5\n```\n\n- **Comportamiento**: Las operaciones de envío solo bloquean cuando el buffer está lleno\n- **Analogía**: Como un buzón con capacidad limitada - puedes dejar varias cartas sin esperar\n\n```go\n// Ejemplo de channel con buffer\nfunc main() {\n    ch := make(chan string, 3)\n\n    ch \u003c- \"Uno\"   // No bloquea\n    ch \u003c- \"Dos\"   // No bloquea\n    ch \u003c- \"Tres\"  // No bloquea\n    // ch \u003c- \"Cuatro\" // ¡Bloquearía porque el buffer está lleno!\n\n    fmt.Println(\u003c-ch) // \"Uno\"\n    fmt.Println(\u003c-ch) // \"Dos\"\n    fmt.Println(\u003c-ch) // \"Tres\"\n}\n```\n\n### 2️⃣ Por dirección\n\n#### Bidireccionales (predeterminado)\n\n```go\nch := make(chan string) // Puede enviar y recibir\n```\n\n#### Solo envío (send-only)\n\n```go\nvar sendCh chan\u003c- string // Solo puede enviar\nsendCh = ch // Conversión válida de bidireccional a send-only\n```\n\n#### Solo recepción (receive-only)\n\n```go\nvar receiveCh \u003c-chan string // Solo puede recibir\nreceiveCh = ch // Conversión válida de bidireccional a receive-only\n```\n\n- **Beneficio**: Restricción explícita de operaciones permitidas, lo que mejora la seguridad\n\n```go\n// Ejemplo de canales direccionales\nfunc productor(out chan\u003c- int) {\n    for i := 0; i \u003c 5; i++ {\n        out \u003c- i\n    }\n    close(out)\n}\n\nfunc consumidor(in \u003c-chan int) {\n    for num := range in {\n        fmt.Println(\"Consumido:\", num)\n    }\n}\n\nfunc main() {\n    ch := make(chan int)\n    go productor(ch)\n    consumidor(ch)\n}\n```\n\n## 🔄 Operaciones Fundamentales con Channels\n\n### 1. Envío de datos\n\n```go\nch \u003c- valor // Envía 'valor' al channel 'ch'\n```\n\n- En un channel sin buffer: bloquea hasta que el valor es recibido\n- En un channel con buffer: bloquea solo si el buffer está lleno\n\n### 2. Recepción de datos\n\n```go\nvalor := \u003c-ch      // Asigna el valor recibido a 'valor'\nvalor, ok := \u003c-ch  // 'ok' es false si el canal está cerrado\n\u003c-ch               // Descarta el valor (útil para sincronización)\n```\n\n- Bloquea hasta que haya un valor disponible o el canal se cierre\n\n### 3. Cierre de channels\n\n```go\nclose(ch)  // Señaliza que no se enviarán más valores\n```\n\n- **Importante**: Solo el remitente debe cerrar un canal, nunca el receptor\n- Después de cerrar: las operaciones de recepción devuelven el valor cero del tipo del canal y `ok=false`\n- Enviar a un canal cerrado causa pánico\n\n### 4. Iteración sobre channels\n\n```go\nfor valor := range ch {\n    // Procesa cada valor hasta que el canal se cierre\n    fmt.Println(valor)\n}\n```\n\n- Termina automáticamente cuando el canal se cierra\n\n## 🔀 Select: Coordinación Multi-Channel\n\nLa declaración `select` permite esperar en múltiples operaciones de channel:\n\n```go\nselect {\ncase v1 := \u003c-ch1:\n    fmt.Println(\"Recibido de ch1:\", v1)\ncase v2 := \u003c-ch2:\n    fmt.Println(\"Recibido de ch2:\", v2)\ncase ch3 \u003c- valor:\n    fmt.Println(\"Enviado a ch3\")\ncase \u003c-time.After(1 * time.Second):\n    fmt.Println(\"Timeout después de 1 segundo\")\ndefault:\n    fmt.Println(\"Ninguna operación lista (no bloqueante)\")\n}\n```\n\n- Si múltiples casos están listos, uno se elige al azar\n- Con `default`: la operación es no bloqueante\n- Sin `default`: bloquea hasta que un caso esté listo\n\n```go\n// Ejemplo de select con timeout\nfunc worker(job int) int {\n    // Simulamos trabajo que toma tiempo aleatorio\n    time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)\n    return job * 2\n}\n\nfunc main() {\n    rand.Seed(time.Now().UnixNano())\n\n    ch := make(chan int)\n    go func() {\n        ch \u003c- worker(5)\n    }()\n\n    select {\n    case result := \u003c-ch:\n        fmt.Println(\"El trabajo completó con resultado:\", result)\n    case \u003c-time.After(500 * time.Millisecond):\n        fmt.Println(\"El trabajo tomó demasiado tiempo\")\n    }\n}\n```\n\n## 📝 Patrones Comunes con Channels\n\n### 1️⃣ Señalización de terminación\n\n```go\ndone := make(chan struct{})\n\ngo func() {\n    // Hacer trabajo...\n    done \u003c- struct{}{} // Señaliza terminación\n}()\n\n\u003c-done // Espera la señal\n```\n\n### 2️⃣ Timeout\n\n```go\nselect {\ncase result := \u003c-workCh:\n    // Procesar resultado\ncase \u003c-time.After(5 * time.Second):\n    // Manejar timeout\n}\n```\n\n### 3️⃣ Fan-out (distribución de trabajo)\n\n```go\nfor _, task := range tasks {\n    go worker(task, resultCh)\n}\n```\n\n### 4️⃣ Fan-in (recolección de resultados)\n\n```go\nmerged := make(chan int)\ngo func() {\n    defer close(merged)\n    for i := 0; i \u003c len(channels); i++ {\n        for val := range channels[i] {\n            merged \u003c- val\n        }\n    }\n}()\n```\n\n### 5️⃣ Canal de cancelación con context\n\n```go\nctx, cancel := context.WithCancel(context.Background())\ndefer cancel()\n\ngo worker(ctx, ...)\n```\n\n### 6️⃣ Sincronización con semáforos\n\n```go\n// Limitar concurrencia a 3 operaciones simultáneas\nsem := make(chan struct{}, 3)\n\nfor i := 0; i \u003c 10; i++ {\n    sem \u003c- struct{}{} // Adquiere el semáforo\n\n    go func(i int) {\n        defer func() { \u003c-sem }() // Libera el semáforo\n        heavyWork(i)\n    }(i)\n}\n```\n\n## 💇‍♂️ El Problema del Barbero Dormilón\n\nEl **Barbero Dormilón** es un problema clásico de concurrencia que podemos resolver elegantemente con channels.\n\n### 🧩 Descripción:\n\n- Una barbería con **un barbero**, **una silla de barbero** y **varias sillas de espera**\n- Si no hay clientes, el barbero se duerme\n- Cuando llega un cliente:\n  - Si el barbero está dormido, lo despierta\n  - Si hay sillas disponibles, espera\n  - Si no hay sillas, se va\n\n### 💻 Implementación completa:\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"math/rand\"\n    \"sync\"\n    \"time\"\n)\n\n// Configuración\nconst (\n    sillasEspera    = 3        // Capacidad de la sala de espera\n    tiempoCorte     = 1000     // ms que tarda un corte\n    tiempoLlegada   = 300      // ms promedio entre llegadas de clientes\n    tiempoBarberia  = 10000    // ms que permanece abierta la barbería\n)\n\nfunc main() {\n    rand.Seed(time.Now().UnixNano())\n\n    // Channels para comunicación\n    salaEspera := make(chan int, sillasEspera) // Clientes en espera (bufferizado)\n    barberoDispo := make(chan bool, 1)         // Señal cuando el barbero está disponible\n    cerrado := make(chan bool)                 // Señal cuando cierra la barbería\n    var wg sync.WaitGroup\n\n    // Estadísticas\n    var clientesAtendidos int\n    var clientesPerdidos int\n    var mu sync.Mutex\n\n    // Iniciamos al barbero\n    wg.Add(1)\n    fmt.Println(\"💈 Barbería abierta\")\n    go func() {\n        defer wg.Done()\n\n        for {\n            // Si no hay clientes, el barbero \"se duerme\"\n            fmt.Println(\"💤 Barbero esperando clientes...\")\n\n            select {\n            case id, open := \u003c-salaEspera:\n                if !open {\n                    fmt.Println(\"🏁 Barbero termina su jornada\")\n                    return\n                }\n\n                // Atender al cliente\n                fmt.Printf(\"💇‍♂️ Barbero atendiendo al cliente %d\\n\", id)\n                time.Sleep(time.Duration(rand.Intn(tiempoCorte/2) + tiempoCorte/2) * time.Millisecond)\n                fmt.Printf(\"✅ Cliente %d atendido\\n\", id)\n\n                mu.Lock()\n                clientesAtendidos++\n                mu.Unlock()\n            }\n        }\n    }()\n\n    // Generamos clientes\n    go func() {\n        clienteID := 1\n        end := time.After(time.Duration(tiempoBarberia) * time.Millisecond)\n\n        // Generamos clientes hasta que cierre la barbería\n        for {\n            select {\n            case \u003c-end:\n                fmt.Println(\"🔒 Barbería cerrando sus puertas\")\n                close(cerrado)\n                return\n            case \u003c-time.After(time.Duration(rand.Intn(tiempoLlegada*2)) * time.Millisecond):\n                // Llegó un nuevo cliente\n                id := clienteID\n                clienteID++\n\n                // Cliente intenta entrar a la sala de espera\n                select {\n                case salaEspera \u003c- id:\n                    fmt.Printf(\"👨 Cliente %d entró a la sala de espera\\n\", id)\n                default:\n                    // Sala de espera llena\n                    mu.Lock()\n                    clientesPerdidos++\n                    mu.Unlock()\n                    fmt.Printf(\"🚶 Cliente %d se fue, sala de espera llena\\n\", id)\n                }\n            }\n        }\n    }()\n\n    // Esperamos a que cierre la barbería\n    \u003c-cerrado\n\n    // Atendemos a los clientes restantes\n    fmt.Println(\"⌛ Atendiendo a los últimos clientes...\")\n    close(salaEspera)\n\n    // Esperamos que terminen todas las goroutines\n    wg.Wait()\n\n    // Mostramos estadísticas\n    fmt.Println(\"\\n📊 ESTADÍSTICAS FINALES\")\n    fmt.Printf(\"✅ Clientes atendidos: %d\\n\", clientesAtendidos)\n    fmt.Printf(\"❌ Clientes perdidos: %d\\n\", clientesPerdidos)\n    fmt.Printf(\"💰 Ingresos del día: $%d\\n\", clientesAtendidos*20)\n    fmt.Println(\"💈 Barbería cerrada por hoy\")\n}\n```\n\n### 🔑 Puntos clave del ejemplo:\n\n1. **`salaEspera` como channel bufferizado**: Representa las sillas de espera con capacidad limitada\n2. **`select` con `default`**: Permite implementar la llegada no bloqueante de clientes (se van si está lleno)\n3. **Cierre de channel**: Señaliza que no llegarán más clientes\n4. **Coordinación con WaitGroup**: Asegura que todos los clientes sean atendidos antes de salir\n\n## 🗣️ Ejemplo Avanzado: Sistema de Chat con Channels\n\nUn ejemplo práctico que muestra el poder de los channels en una aplicación más compleja:\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"sync\"\n    \"time\"\n)\n\n// Tipo de mensaje\ntype Message struct {\n    sender    string\n    content   string\n    timestamp time.Time\n}\n\n// Cliente de chat\ntype Client struct {\n    name      string\n    incoming  chan Message    // Mensajes entrantes\n    outgoing  chan\u003c- Message  // Mensajes salientes\n    quit      chan struct{}   // Señal para desconectar\n}\n\n// Centro de distribución de mensajes\ntype Hub struct {\n    clients    map[string]*Client\n    register   chan *Client\n    unregister chan string\n    broadcast  chan Message\n    mutex      sync.RWMutex\n}\n\n// Inicializar un nuevo hub\nfunc NewHub() *Hub {\n    return \u0026Hub{\n        clients:    make(map[string]*Client),\n        register:   make(chan *Client),\n        unregister: make(chan string),\n        broadcast:  make(chan Message),\n    }\n}\n\n// Ejecutar el hub\nfunc (h *Hub) Run() {\n    for {\n        select {\n        case client := \u003c-h.register:\n            // Registrar nuevo cliente\n            h.mutex.Lock()\n            h.clients[client.name] = client\n            h.mutex.Unlock()\n            fmt.Printf(\"👋 %s se ha conectado\\n\", client.name)\n\n        case name := \u003c-h.unregister:\n            // Eliminar cliente\n            h.mutex.Lock()\n            if client, ok := h.clients[name]; ok {\n                close(client.incoming)\n                delete(h.clients, name)\n            }\n            h.mutex.Unlock()\n            fmt.Printf(\"👋 %s se ha desconectado\\n\", name)\n\n        case msg := \u003c-h.broadcast:\n            // Distribuir mensaje a todos los clientes excepto al remitente\n            h.mutex.RLock()\n            for name, client := range h.clients {\n                if name != msg.sender {\n                    select {\n                    case client.incoming \u003c- msg:\n                        // Mensaje enviado exitosamente\n                    default:\n                        // Canal lleno, desregistramos al cliente\n                        h.mutex.RUnlock()\n                        h.unregister \u003c- name\n                        h.mutex.RLock()\n                    }\n                }\n            }\n            h.mutex.RUnlock()\n        }\n    }\n}\n\n// Crear nuevo cliente\nfunc NewClient(name string, hub *Hub) *Client {\n    client := \u0026Client{\n        name:     name,\n        incoming: make(chan Message, 10),\n        outgoing: hub.broadcast,\n        quit:     make(chan struct{}),\n    }\n\n    hub.register \u003c- client\n    return client\n}\n\n// Enviar mensaje\nfunc (c *Client) Send(content string) {\n    msg := Message{\n        sender:    c.name,\n        content:   content,\n        timestamp: time.Now(),\n    }\n    c.outgoing \u003c- msg\n    fmt.Printf(\"[%s] %s: %s\\n\",\n              msg.timestamp.Format(\"15:04:05\"), c.name, content)\n}\n\n// Procesar mensajes entrantes\nfunc (c *Client) HandleMessages() {\n    for {\n        select {\n        case msg, ok := \u003c-c.incoming:\n            if !ok {\n                return\n            }\n            fmt.Printf(\"[%s] 📬 %s recibió de %s: %s\\n\",\n                msg.timestamp.Format(\"15:04:05\"), c.name, msg.sender, msg.content)\n\n        case \u003c-c.quit:\n            return\n        }\n    }\n}\n\n// Desconectar cliente\nfunc (c *Client) Disconnect(hub *Hub) {\n    hub.unregister \u003c- c.name\n    close(c.quit)\n}\n\nfunc main() {\n    hub := NewHub()\n    go hub.Run()\n\n    // Creamos clientes\n    alice := NewClient(\"Alice\", hub)\n    bob := NewClient(\"Bob\", hub)\n    charlie := NewClient(\"Charlie\", hub)\n\n    // Manejamos mensajes en goroutines\n    go alice.HandleMessages()\n    go bob.HandleMessages()\n    go charlie.HandleMessages()\n\n    // Simulamos una conversación\n    time.Sleep(500 * time.Millisecond)\n\n    alice.Send(\"¡Hola a todos!\")\n    time.Sleep(800 * time.Millisecond)\n\n    bob.Send(\"Hola Alice, ¿cómo estás?\")\n    time.Sleep(1000 * time.Millisecond)\n\n    charlie.Send(\"¡Yo también estoy aquí!\")\n    time.Sleep(700 * time.Millisecond)\n\n    // Bob se desconecta\n    bob.Disconnect(hub)\n    time.Sleep(500 * time.Millisecond)\n\n    alice.Send(\"¿Bob sigues ahí?\")\n    time.Sleep(1000 * time.Millisecond)\n\n    // Limpieza\n    alice.Disconnect(hub)\n    charlie.Disconnect(hub)\n\n    fmt.Println(\"Demostración finalizada\")\n}\n```\n\n### 🔍 Conceptos avanzados demostrados:\n\n- **Canales como propiedades de structs**\n- **Canales direccionales**\n- **Select para manejo de múltiples canales**\n- **Patrón de registro/desregistro**\n- **Comunicación broadcast**\n- **Señalización para terminación**\n- **Manejo de cierres de canales**\n\n## ⚠️ Errores Comunes y Mejores Prácticas\n\n### 🚫 Errores comunes:\n\n1. **Deadlock**: Todas las goroutines bloqueadas\n\n   ```go\n   ch := make(chan int)\n   ch \u003c- 1  // ¡Deadlock! Nadie está recibiendo\n   ```\n\n2. **Enviar a un canal cerrado**: Causa panic\n\n   ```go\n   close(ch)\n   ch \u003c- 42  // panic: send on closed channel\n   ```\n\n3. **Olvidar cerrar canales**: Causa fugas de memoria o goroutines bloqueadas\n\n4. **Cerrar un canal más de una vez**: Causa panic\n\n   ```go\n   close(ch)\n   close(ch)  // panic: close of closed channel\n   ```\n\n5. **Cerrar canales desde el receptor**: Va contra el patrón recomendado\n\n### ✅ Mejores prácticas:\n\n1. **El emisor debe cerrar el canal**, no el receptor\n\n2. **Usa canales direccionales** en firmas de funciones para claridad\n\n   ```go\n   func productor(out chan\u003c- int) { /* ... */ }\n   func consumidor(in \u003c-chan int) { /* ... */ }\n   ```\n\n3. **Documenta quién es responsable** de cerrar cada canal\n\n4. **Usa context para cancelación** en sistemas complejos\n\n   ```go\n   ctx, cancel := context.WithCancel(context.Background())\n   defer cancel()\n   ```\n\n5. **Prefiere canales sin buffer** a menos que necesites específicamente el buffering\n\n6. **Usa nil channels** para deshabilitar casos en select\n\n   ```go\n   var ch chan int // ch es nil\n   select {\n   case \u003c-ch:       // Este caso nunca será seleccionado\n   case \u003c-otroCanal:\n       // ...\n   }\n   ```\n\n7. **Usa `for-range`** para consumir canales hasta que se cierren\n   ```go\n   for valor := range ch {\n       // Procesa cada valor\n   }\n   ```\n\n## 🔀 Ejemplo: Ping-Pong con Channels\n\nUn ejemplo simple pero completo que ilustra la comunicación bidireccional:\n\n```go\npackage main\n\nimport (\n    \"fmt\"\n    \"strings\"\n    \"time\"\n)\n\n// shout recibe del canal ping y envía al canal pong\nfunc shout(ping \u003c-chan string, pong chan\u003c- string) {\n    for {\n        // Recibe mensaje, verificando si el canal está cerrado\n        msg, ok := \u003c-ping\n        if !ok {\n            fmt.Println(\"🔇 Canal ping cerrado, terminando\")\n            return\n        }\n\n        // Procesa y envía respuesta\n        time.Sleep(500 * time.Millisecond)\n        respuesta := fmt.Sprintf(\"🔊 ECO: %s!!!\", strings.ToUpper(msg))\n        pong \u003c- respuesta\n    }\n}\n\nfunc main() {\n    fmt.Println(\"🏓 Iniciando ejemplo de Ping-Pong\")\n\n    // Crear canales\n    ping := make(chan string)\n    pong := make(chan string)\n\n    // Iniciar goroutine\n    go shout(ping, pong)\n\n    fmt.Println(\"✏️  Escribe algo (q para salir):\")\n\n    for {\n        fmt.Print(\"👉 \")\n\n        var entrada string\n        fmt.Scanln(\u0026entrada)\n\n        if strings.ToLower(entrada) == \"q\" {\n            fmt.Println(\"👋 Saliendo...\")\n            break\n        }\n\n        // Enviar y recibir\n        ping \u003c- entrada\n        respuesta := \u003c-pong\n        fmt.Println(respuesta)\n    }\n\n    fmt.Println(\"🏁 Terminando, cerrando canales\")\n    close(ping)\n    // No cerramos pong para evitar potenciales panics\n}\n```\n\n## 🌟 Consejos finales\n\n1. **Piensa en los channels como transferencia de propiedad**: Cuando envías un valor, estás cediendo la propiedad a quien lo recibe\n\n2. **Comienza con la solución más simple**: Generalmente canales sin buffer y patrones básicos\n\n3. **Usa channels para comunicación, mutexes para estado compartido**\n\n4. **Prueba con `-race` detector**: Ayuda a encontrar race conditions\n\n5. **El cierre del canal es una señal importante**: Representa \"fin de datos\" o \"completado\"\n\nLos channels son la característica distintiva del modelo de concurrencia de Go. Dominarlos te permitirá escribir código concurrente limpio, seguro y eficiente, aprovechando al máximo lo que el lenguaje tiene para ofrecer.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdieg0code%2Fgolang_concurrecy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdieg0code%2Fgolang_concurrecy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdieg0code%2Fgolang_concurrecy/lists"}