CAP 10 · LEC 01·Patrones idiomáticos

Generics: type parameters y constraints

Desde Go 1.18 el lenguaje soporta generics: funciones y tipos parametrizados por tipos. Permiten escribir código reutilizable sin sacrificar el tipado estático ni recurrir a `interface{}` y type assertions.

● AVANZADO8 min lecturapor Fernando Herrera · actualizado mayo de 2026
¿Encontraste un error o algo que mejorar?Editá esta lección en GitHub →

Por qué generics

Antes de Go 1.18, escribir una función Map o Filter reutilizable obligaba a usar interface{} (hoy any) y type assertions en cada operación, perdiendo el chequeo estático. Generics resuelven esto introduciendo type parameters: variables que representan tipos y son resueltas en tiempo de compilación.

package main import "fmt" // Antes de generics: una función por tipo o usar interface{} func SumInts(nums []int) int { total := 0 for _, n := range nums { total += n } return total } func SumFloats(nums []float64) float64 { total := 0.0 for _, n := range nums { total += n } return total } // Con generics: una sola función para cualquier tipo numérico func Sum[T int | float64](nums []T) T { var total T // zero value del tipo concreto for _, n := range nums { total += n } return total } func main() { ints := []int{1, 2, 3, 4} floats := []float64{1.5, 2.5, 3.0} fmt.Println(SumInts(ints)) // 10 fmt.Println(SumFloats(floats)) // 7 fmt.Println(Sum(ints)) // 10 (T = int inferido) fmt.Println(Sum(floats)) // 7 (T = float64 inferido) }
Salida10 7 10 7

Type parameters: la sintaxis [T any]

Los type parameters se declaran entre corchetes después del nombre de la función o tipo. Cada parámetro tiene una constraint que define qué tipos son válidos. La constraint más permisiva es any (alias de interface{}).

package main import "fmt" // T es un type parameter con constraint 'any' (cualquier tipo) // Map convierte []T en []U aplicando una función de transformación func Map[T, U any](s []T, f func(T) U) []U { result := make([]U, len(s)) for i, v := range s { result[i] = f(v) } return result } // Filter conserva solo los elementos para los que predicate retorna true func Filter[T any](s []T, predicate func(T) bool) []T { result := make([]T, 0, len(s)) for _, v := range s { if predicate(v) { result = append(result, v) } } return result } func main() { nums := []int{1, 2, 3, 4, 5} // Map int -> string strs := Map(nums, func(n int) string { return fmt.Sprintf("n=%d", n) }) fmt.Println(strs) // [n=1 n=2 n=3 n=4 n=5] // Map int -> int (cuadrados) squares := Map(nums, func(n int) int { return n * n }) fmt.Println(squares) // [1 4 9 16 25] // Filter evens := Filter(nums, func(n int) bool { return n%2 == 0 }) fmt.Println(evens) // [2 4] }
Salida[n=1 n=2 n=3 n=4 n=5] [1 4 9 16 25] [2 4]
Inferencia de tipos

Go infiere los type parameters a partir de los argumentos. Map(nums, f) deduce T = int y U desde el retorno de f, así que rara vez hace falta escribir Map[int, string](nums, f) de forma explícita.

Constraints: any, comparable y conjuntos de tipos

Una constraint es una interfaz especial que define qué operaciones se pueden hacer con el type parameter. Go provee dos predefinidas: any (cualquier tipo) y comparable (tipos que soportan == y !=). Para operaciones aritméticas hay que definir constraints propias.

package main import "fmt" // 'comparable' permite usar == y != // Útil para keys de mapas, búsqueda en slices, etc. func Contains[T comparable](s []T, target T) bool { for _, v := range s { if v == target { return true } } return false } // Conjunto de tipos con | (union) // El ~ (tilde) incluye tipos definidos a partir de int // Ej: type Celsius int también satisface ~int type Number interface { ~int | ~int64 | ~float32 | ~float64 } func Max[T Number](a, b T) T { if a > b { return a } return b } type Celsius int // tipo derivado de int func main() { fmt.Println(Contains([]int{1, 2, 3}, 2)) // true fmt.Println(Contains([]string{"a", "b"}, "z")) // false fmt.Println(Max(3, 7)) // 7 fmt.Println(Max(2.5, 1.1)) // 2.5 fmt.Println(Max(Celsius(20), Celsius(30))) // 30 (gracias a ~int) }
Salidatrue false 7 2.5 30
El operador ~ (tilde)

~int significa "cualquier tipo cuyo underlying type sea int". Sin la tilde, type Celsius int no satisfaría una constraint int. Es casi siempre lo que querés al diseñar constraints numéricas.

Tipos genéricos: structs parametrizados

Los type parameters también se aplican a tipos. Un Stack[T any] o Set[T comparable] son ejemplos clásicos. Cada instancia se monomorfiza con un tipo concreto en compilación.

package main import "fmt" // Stack genérico: una pila de elementos de tipo T type Stack[T any] struct { items []T } func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) } func (s *Stack[T]) Pop() (T, bool) { var zero T // zero value de T cuando la pila está vacía if len(s.items) == 0 { return zero, false } last := len(s.items) - 1 v := s.items[last] s.items = s.items[:last] return v, true } func (s *Stack[T]) Len() int { return len(s.items) } // Set genérico: solo acepta tipos comparables (para usarlos como key de map) type Set[T comparable] struct { data map[T]struct{} } func NewSet[T comparable]() *Set[T] { return &Set[T]{data: make(map[T]struct{})} } func (s *Set[T]) Add(v T) { s.data[v] = struct{}{} } func (s *Set[T]) Has(v T) bool { _, ok := s.data[v]; return ok } func (s *Set[T]) Len() int { return len(s.data) } func main() { // Stack de strings stack := &Stack[string]{} stack.Push("uno") stack.Push("dos") stack.Push("tres") for stack.Len() > 0 { v, _ := stack.Pop() fmt.Println(v) } // Set de ints set := NewSet[int]() set.Add(1) set.Add(2) set.Add(2) // duplicado, se ignora fmt.Println(set.Len(), set.Has(2), set.Has(99)) }
Salidatres dos uno 2 true false

Cuándo NO usar generics

Generics son una herramienta poderosa, pero no son la respuesta a todo. Go favorece la simplicidad: si una interfaz tradicional resuelve el problema con claridad, suele ser la mejor opción.

Reglas prácticas
  • Si necesitás llamar un método sobre los valores, usá una interfaz, no generics.
  • Si la lógica solo aplica a un tipo concreto, no parametrices "por si acaso".
  • Los generics brillan en estructuras de datos (Stack, Tree, Cache) y en helpers funcionales (Map, Filter, Reduce) donde la operación es agnóstica al tipo.
  • El equipo de Go recomienda: "Si te encontrás escribiendo el mismo código para varios tipos sin cambios significativos, considerá generics. Si las implementaciones difieren por tipo, no."