Generics en Go en producción: Optional, Result, Set, filter/map/reduce y patrones reales

Los generics de Go (introducidos en 1.18) llevan varios años en producción y ya hay patrones establecidos de qué funciona bien y qué no. Los casos de uso que más valor aportan son los contenedores seguros en tipos, las funciones funcionales sin boxing y los tipos de error/resultado explícitos.

Optional: alternativa segura a punteros

package optional

type Optional[T any] struct {
    value *T
}

func Some[T any](v T) Optional[T] {
    return Optional[T]{value: &v}
}

func None[T any]() Optional[T] {
    return Optional[T]{}
}

func (o Optional[T]) IsPresent() bool { return o.value != nil }
func (o Optional[T]) Get() (T, bool) {
    if o.value == nil {
        var zero T
        return zero, false
    }
    return *o.value, true
}
func (o Optional[T]) OrElse(def T) T {
    if o.value == nil {
        return def
    }
    return *o.value
}

// Uso:
func buscarUsuario(id int) optional.Optional[Usuario] {
    // base de datos...
    if encontrado {
        return optional.Some(u)
    }
    return optional.None[Usuario]()
}

u := buscarUsuario(1)
nombre := u.Map(func(u Usuario) string { return u.Nombre }).OrElse("Desconocido")

Result: errores explícitos sin panic

type Result[T any] struct {
    value T
    err   error
}

func Ok[T any](v T) Result[T]       { return Result[T]{value: v} }
func Err[T any](e error) Result[T]  { return Result[T]{err: e} }

func (r Result[T]) IsOk() bool         { return r.err == nil }
func (r Result[T]) Unwrap() T          {
    if r.err != nil { panic(r.err) }
    return r.value
}
func (r Result[T]) UnwrapOr(def T) T {
    if r.err != nil { return def }
    return r.value
}
func (r Result[T]) Error() error       { return r.err }

// Uso encadenado:
func dividir(a, b float64) Result[float64] {
    if b == 0 {
        return Err[float64](errors.New("división por cero"))
    }
    return Ok(a / b)
}

r := dividir(10, 2)
if r.IsOk() {
    fmt.Println(r.Unwrap()) // 5
}

Set genérico con Union e Intersection

type Set[T comparable] struct {
    items map[T]struct{}
}

func NewSet[T comparable](items ...T) Set[T] {
    s := Set[T]{items: make(map[T]struct{})}
    for _, item := range items {
        s.items[item] = struct{}{}
    }
    return s
}

func (s Set[T]) Add(item T)         { s.items[item] = struct{}{} }
func (s Set[T]) Contains(item T) bool { _, ok := s.items[item]; return ok }
func (s Set[T]) Len() int           { return len(s.items) }

func Union[T comparable](a, b Set[T]) Set[T] {
    result := NewSet[T]()
    for k := range a.items { result.Add(k) }
    for k := range b.items { result.Add(k) }
    return result
}

func Intersection[T comparable](a, b Set[T]) Set[T] {
    result := NewSet[T]()
    for k := range a.items {
        if b.Contains(k) {
            result.Add(k)
        }
    }
    return result
}

// Uso:
admins := NewSet("alice", "bob")
activos := NewSet("bob", "carol", "dave")
adminsActivos := Intersection(admins, activos)
fmt.Println(adminsActivos.Contains("bob"))   // true
fmt.Println(adminsActivos.Contains("alice")) // false

Filter, Map y Reduce sin boxing con benchmarks

func Filter[T any](s []T, fn func(T) bool) []T {
    result := make([]T, 0, len(s))
    for _, v := range s {
        if fn(v) {
            result = append(result, v)
        }
    }
    return result
}

func Map[T, U any](s []T, fn func(T) U) []U {
    result := make([]U, len(s))
    for i, v := range s {
        result[i] = fn(v)
    }
    return result
}

func Reduce[T, U any](s []T, init U, fn func(U, T) U) U {
    acc := init
    for _, v := range s {
        acc = fn(acc, v)
    }
    return acc
}

// Uso sin conversiones ni interface{}:
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
pares := Filter(nums, func(n int) bool { return n%2 == 0 })
cuadrados := Map(pares, func(n int) int { return n * n })
suma := Reduce(cuadrados, 0, func(acc, n int) int { return acc + n })
fmt.Println(suma) // 220

Qué no puedes hacer: type switch sobre T

El compilador de Go no permite hacer un type switch sobre un parámetro de tipo genérico:

// ESTO NO COMPILA:
func procesar[T any](v T) {
    switch v.(type) { // error: cannot use type switch on type parameter
    case int:
        ...
    case string:
        ...
    }
}

// SOLUCIÓN: usar reflect o interfaces con método
type Procesable interface {
    Procesar() string
}

func procesar[T Procesable](v T) string {
    return v.Procesar()
}

Cuándo usar generics frente a interfaces

La regla práctica es: usa interfaces cuando el comportamiento varía en runtime; usa generics cuando el tipo varía en compilación pero el algoritmo es el mismo. Los generics son especialmente útiles para contenedores (Set, Queue, Stack), funciones de colección (Filter, Map) y wrappers de tipos de resultado. Evítalos cuando la restricción de tipo es tan amplia que el código genérico no aporta más que el código con any e introspección.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP