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.
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)
}10
7
10
7Type 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]
}[n=1 n=2 n=3 n=4 n=5]
[1 4 9 16 25]
[2 4]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)
}true
false
7
2.5
30~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))
}tres
dos
uno
2 true falseCuá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.
- 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."