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.
