Patrones de diseño avanzados en Go: de la teoría a la práctica
Go (Golang) es un lenguaje que fomenta la simplicidad y legibilidad del código. Sin embargo, cuando un proyecto crece, incluso el código más conciso puede convertirse en un caos sin una arquitectura adecuada. A nivel avanzado, los desarrolladores de Go utilizan activamente patrones de diseño adaptados a las características del lenguaje: gorutinas, canales, interfaces y genéricos (desde Go 1.18+).
En este artículo, analizaremos 4 patrones potentes que te ayudarán a escribir código escalable, testeable y de alto rendimiento en Go. Cada patrón incluye un ejemplo práctico.
1. Patrón Pipeline (Tubería) para procesamiento de datos en flujo
Pipeline es uno de los patrones más populares en Go para trabajar con procesamiento secuencial de datos. Se adapta perfectamente al modelo de gorutinas y canales. Cada etapa de procesamiento se ejecuta en una gorutina separada, y los datos se transmiten a través de canales.
Cuándo aplicarlo: Procesamiento de logs, transformación de datos, procesos ETL, video en flujo.
Ejemplo: Tubería para filtrar y transformar números.
package main
import "fmt"
// Stage 1: Generación de númerosfunc generate(nums ...int) <-chan int { out := make(chan int) go func() { for _, n := range nums { out <- n } close(out) }() return out}
// Stage 2: Multiplicación por 2func multiply(in <-chan int) <-chan int { out := make(chan int) go func() { for n := range in { out <- n * 2 } close(out) }() return out}
// Stage 3: Filtrado (solo pares)func filterEven(in <-chan int) <-chan int { out := make(chan int) go func() { for n := range in { if n%2 == 0 { out <- n } } close(out) }() return out}
func main() { // Construimos la tubería pipeline := filterEven(multiply(generate(1, 2, 3, 4, 5)))
for result := range pipeline { fmt.Println(result) // Salida: 4, 8 (solo pares después de la multiplicación) }}Ventajas: Procesamiento paralelo natural, bajo consumo de memoria (los datos no se acumulan), fácil de añadir nuevas etapas.
2. Fan-Out / Fan-In: escalando el procesamiento
Este patrón es una evolución avanzada de Pipeline. Fan-Out (bifurcación) lanza múltiples gorutinas para procesar datos en paralelo desde un solo canal. Fan-In (fusión) recoge los resultados de varios canales en uno solo.
Cuándo aplicarlo: Parseo de páginas web, procesamiento de grandes cantidades de archivos, solicitudes paralelas a APIs.
Ejemplo: Verificación paralela de disponibilidad de URLs.
package main
import ( "fmt" "net/http" "sync")
func worker(urls <-chan string, results chan<- string, wg *sync.WaitGroup) { defer wg.Done() for url := range urls { resp, err := http.Get(url) if err != nil { results <- fmt.Sprintf("Error: %s - %v", url, err) continue } resp.Body.Close() results <- fmt.Sprintf("OK: %s - %d", url, resp.StatusCode) }}
func fanOutFanIn(urls []string, numWorkers int) []string { urlChan := make(chan string, len(urls)) resultChan := make(chan string, len(urls)) var wg sync.WaitGroup
// Fan-Out: lanzamos workers for i := 0; i < numWorkers; i++ { wg.Add(1) go worker(urlChan, resultChan, &wg) }
// Enviamos URLs for _, url := range urls { urlChan <- url } close(urlChan)
// Esperamos a que todos los workers terminen wg.Wait() close(resultChan)
// Fan-In: recogemos los resultados var results []string for res := range resultChan { results = append(results, res) } return results}