Antes de ver el código, un pequeño baño de realidad: en el mundo real, calcular un factorial secuencialmente en un solo hilo es mucho más rápido, ya que el costo de crear goroutines y canales es mayor que el de hacer unas cuantas multiplicaciones. Sin embargo, como ejercicio pedagógico para entender la concurrencia en Go, es un ejemplo fantástico.
Aquí tienes cómo dividirías el trabajo matemático utilizando goroutines y sincronizando el resultado final a través de un canal.
Calculando un Factorial con Goroutines y Channels
Vamos a calcular el factorial de 10 ($10!$). Para hacerlo concurrente, dividiremos el trabajo en dos mitades:
- Una goroutine calculará la multiplicación del 1 al 5.
- Otra goroutine calculará la multiplicación del 6 al 10.
- El canal recibirá ambas partes y las multiplicará en la función principal.
Go
¿Por qué usamos channels aquí y cómo funcionan?
- Comunicación sin compartir memoria: En lugar de crear una variable global
totaly usar unsync.Mutexpara bloquearla y evitar condiciones de carrera (Race Conditions) cuando ambas goroutines intenten escribir en ella, los canales permiten que cada goroutine haga su trabajo aislada y simplemente “entregue” el paquete de vuelta. - Sincronización implícita: Las líneas
part1 := <-chypart2 := <-chactúan como una barrera. El programa principal (main) se pausa automáticamente en ese punto exacto y no terminará hasta que ambas goroutines hayan terminado sus cálculos y enviado sus respuestas a través del canal.
package main
import (
"fmt"
)
// partialFactorial calcula la multiplicación de un rango de números
// y envía el resultado al canal 'ch'.
func partialFactorial(start, end int64, ch chan<- int64) {
var result int64 = 1
for i := start; i <= end; i++ {
result *= i
}
// Enviamos el resultado parcial al canal
ch <- result
}
func main() {
number := int64(10)
mid := number / 2
// Creamos un canal que transportará números int64.
// Usamos un buffer de 2 porque esperamos dos resultados.
ch := make(chan int64, 2)
// Lanzamos la primera goroutine (calcula 1 * 2 * 3 * 4 * 5)
go partialFactorial(1, mid, ch)
// Lanzamos la segunda goroutine (calcula 6 * 7 * 8 * 9 * 10)
go partialFactorial(mid+1, number, ch)
// Sincronización:
// La función main se bloqueará aquí hasta que el canal reciba datos.
part1 := <-ch
part2 := <-ch
// Multiplicamos ambas partes para obtener el resultado final
total := part1 * part2
fmt.Printf("El factorial de %d es: %d\n", number, total)
}
GoAlgoritmo para el Cifrado Cesar en Go, común cuando inicias en criptografía y fácil de implementar, solo que en momentos de presión puedes llegar a hacer algo complicado cuando en realidad lo puedes resolver con un poco de aritmética (simple).
// Filtramos el slice sin crear copias extras
n := 0
for _, x := range slice {
if keep(x) { // Condición para mantener el elemento
slice[n] = x
n++
}
}
slice = slice[:n] // Ajustamos el tamaño finalAlgoritmo para el Cifrado Cesar en Go, común cuando inicias en criptografía y fácil de implementar, solo que en momentos de presión puedes llegar a hacer algo complicado cuando en realidad lo puedes resolver con un poco de aritmética (simple).
package main
import "fmt"
type task struct {
title string
descripcion string
ok bool
createAt int64
}
func main() {
// 1. Datos de prueba
tasks := []task{
{title: "Aprender Go", ok: true},
{title: "Ir al gym", ok: false},
{title: "Mejorar Inglés", ok: true},
{title: "Proyecto personal", ok: false},
}
fmt.Println("Antes:", len(tasks))
// 2. EL ALGORITMO DE FILTRADO
n := 0
for i := range tasks {
// "Si la tarea NO está lista (ok == false), la mantenemos"
if !tasks[i].ok {
tasks[n] = tasks[i]
n++
}
}
// 3. RE-SLICING: Ajustamos el tamaño al nuevo conteo
tasks = tasks[:n]
// 4. Resultado
fmt.Println("Después:", len(tasks))
for _, v := range tasks {
fmt.Printf("- %s (Estado: %v)\n", v.title, v.ok)
}
}
package main
import (
"reflect"
"testing"
)
func TestFilterTasksInPlace(t *testing.T) {
// Definimos los casos de prueba (Table-driven tests)
tests := []struct {
name string
input []task
expected []task
}{
{
name: "Filtrar algunas completadas",
input: []task{
{title: "T1", ok: true},
{title: "T2", ok: false},
{title: "T3", ok: true},
},
expected: []task{
{title: "T2", ok: false},
},
},
{
name: "Todas completadas (debe quedar vacío)",
input: []task{
{title: "T1", ok: true},
{title: "T2", ok: true},
},
expected: []task{},
},
{
name: "Ninguna completada (debe quedar igual)",
input: []task{
{title: "T1", ok: false},
{title: "T2", ok: false},
},
expected: []task{
{title: "T1", ok: false},
{title: "T2", ok: false},
},
},
{
name: "Slice vacío",
input: []task{},
expected: []task{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Ejecutamos tu algoritmo
result := FilterTasksInPlace(tt.input)
// Comparamos el resultado con lo esperado
// reflect.DeepEqual es genial para comparar slices/structs
if !reflect.DeepEqual(result, tt.expected) {
t.Errorf("error en %s: esperado %v, obtenido %v", tt.name, tt.expected, result)
}
})
}
}