Introducción: qué son los Generics y por qué aparecieron en Go
Durante mucho tiempo, la comunidad de Go prescindió de los genéricos (tipos parametrizados). Los desarrolladores usaban interface{} y conversiones de tipo manuales, lo que generaba código verboso y errores en tiempo de ejecución. Con el lanzamiento de la versión Go 1.18 (febrero de 2022), se añadieron oficialmente al lenguaje los genéricos, una de las funciones más esperadas.
Los Generics (o polimorfismo paramétrico) permiten escribir funciones y estructuras de datos que funcionan con cualquier tipo, manteniendo al mismo tiempo un tipado estricto en tiempo de compilación. Esto significa que puedes escribir un algoritmo (por ejemplo, ordenación o búsqueda) y usarlo para int, string, estructuras personalizadas, sin duplicar código y sin perder la seguridad de tipos.
En este artículo, analizaremos para qué sirven los genéricos en Go, cómo usarlos correctamente y qué aspectos tener en cuenta.
Parte principal: cómo funcionan los Generics en Go
1. Sintaxis de los parámetros de tipo
La sintaxis básica de los genéricos en Go es la siguiente: después del nombre de la función o tipo, entre corchetes, se indican los parámetros de tipo. Veamos un ejemplo muy simple: una función que devuelve el valor recibido (análogo a identity):
package main
import "fmt"
// Función genérica: T es un parámetro de tipofunc Print[T any](value T) { fmt.Println(value)}
func main() { Print[int](42) // indicación explícita del tipo Print[string]("Go") // indicación explícita del tipo Print("Go 1.18") // inferencia de tipo implícita (type inference)}La palabra clave any es un alias para interface{}. Significa que la función acepta un valor de cualquier tipo. Sin embargo, el verdadero poder de los genéricos se revela al usar restricciones (constraints).
2. Restricciones (constraints) e interfaces
Las restricciones permiten especificar qué operaciones se pueden realizar con el parámetro de tipo. Por ejemplo, para sumar dos números, es necesario garantizar que el tipo soporte los operadores +, -, *, etc. En Go, para esto se utilizan interfaces especiales del paquete golang.org/x/exp/constraints o interfaces personalizadas:
package main
import ( "fmt" "golang.org/x/exp/constraints")
// Restricción: T debe soportar comparaciónfunc Max[T constraints.Ordered](a, b T) T { if a > b { return a } return b}
func main() { fmt.Println(Max(10, 20)) // 20 fmt.Println(Max("apple", "banana")) // banana // fmt.Println(Max([]int{1}, []int{2})) // Error de compilación: los slices no están ordenados}La interfaz incorporada constraints.Ordered incluye tipos que soportan los operadores <, <=, >, >=: todos los tipos numéricos y string.
También puedes crear tus propias restricciones:
// Restricción personalizada: el tipo debe tener un método String()type Stringer interface { String() string}
func Describe[T Stringer](v T) string { return "Value: " + v.String()}3. Estructuras y métodos genéricos
Los genéricos en Go funcionan no solo con funciones, sino también con tipos personalizados. Veamos un ejemplo clásico: una pila genérica:
package main
import "fmt"
// Estructura genérica Stacktype Stack[T any] struct { items []T}
// Push añade un elemento a la pilafunc (s *Stack[T]) Push(item T) { s.items = append(s.items, item)}
// Pop extrae un elemento de la pilafunc (s *Stack[T]) Pop() (T, bool) { if len(s.items) == 0 { var zero T return zero, false } last := s.items[len(s.items)-1] s.items = s.items[:len(s.items)-1] return last, true}
func main()