Introducción: ¿Qué son los Generics y por qué aparecieron en Go?
\nDurante mucho tiempo, el lenguaje Go (Golang) fue uno de los pocos lenguajes modernos sin soporte para programación genérica (generics). Los desarrolladores tenían que duplicar código para diferentes tipos de datos o usar interfaces y interface{} vacíos, lo que provocaba pérdida de seguridad de tipos y la necesidad de realizar conversiones de tipos en tiempo de ejecución.
Con el lanzamiento de la versión Go 1.18 (febrero de 2022), el lenguaje incorporó los tan esperados genéricos. Generics es un mecanismo que permite escribir funciones, tipos y estructuras de datos que funcionan con tipos arbitrarios, manteniendo una tipificación estricta en tiempo de compilación.
\nEn este artículo veremos:
\n- \n
- Para qué sirven los genéricos en Go; \n
- Cómo declarar funciones y tipos genéricos; \n
- Qué son los constraints y cómo usarlos; \n
- Ejemplos prácticos de aplicación. \n
¿Para qué sirven los Generics en Go?
\nAntes de la llegada de los genéricos, un problema típico era: necesitas escribir una función que trabaje con slices de diferentes tipos (por ejemplo, int, string, float64).
Sin genéricos tenías que escribir tres funciones separadas:
\nfunc SumInts(nums []int) int {\n total := 0\n for _, v := range nums {\n total += v\n }\n return total\n}\n\nfunc SumFloats(nums []float64) float64 {\n total := 0.0\n for _, v := range nums {\n total += v\n }\n return total\n}\n\nO usar interface{} con conversión de tipos — inseguro e incómodo.
Con genéricos basta con una sola función:
\nfunc Sum[T int | float64](nums []T) T {\n var total T\n for _, v := range nums {\n total += v\n }\n return total\n}\n\nLas ventajas son evidentes:
\n- \n
- Seguridad de tipos — los errores de tipo se detectan en tiempo de compilación; \n
- Reducción de duplicación de código — una función para muchos tipos; \n
- Legibilidad — el código se vuelve más conciso y claro. \n
Fundamentos de la sintaxis de Generics en Go
\nDeclaración de una función genérica
\nLa sintaxis básica usa corchetes [] para indicar los parámetros de tipo:
func NombreFuncion[NombreTipo constraint](parámetros) resultado {\n // cuerpo de la función\n}\n\nEjemplo de una función simple que devuelve el valor recibido:
\nfunc Identity[T any](value T) T {\n return value\n}\n\nfunc main() {\n fmt.Println(Identity[int](42)) // 42\n fmt.Println(Identity[string]("Go")) // Go\n}\n\nGo puede inferir el tipo automáticamente, por lo que la indicación explícita [int] no es obligatoria:
fmt.Println(Identity(42)) // Go entiende que T = int\nfmt.Println(Identity("Go")) // T = string\n\n\nTipos genéricos (estructuras)
\nLos genéricos funcionan no solo con funciones, sino también con tipos definidos por el usuario:
\ntype Stack[T any] struct {\n elements []T\n}\n\nfunc (s *Stack[T]) Push(value T) {\n s.elements = append(s.elements, value)\n}\n\nfunc (s *Stack[T]) Pop() (T, bool) {\n if len(s.elements) == 0 {\n var zero T\n return zero, false\n }\n value := s.elements[len(s.elements)-1]\n s.elements = s.elements[:len(s.elements)-1]\n return value, true\n}\n\nfunc main() {\n intStack := Stack[int]{}\n intStack.Push(10)\n intStack.Push(20)\n val, ok := intStack.Pop()\n fmt.Println(val, ok) // 20 true\n}\n\n\nConstraints: restricciones para tipos
\nEn los ejemplos anteriores usamos any — un constraint incorporado que significa "cualquier tipo". Sin embargo, a menudo es necesario imponer restricciones: por ejemplo, que el tipo soporte comparación u operaciones aritméticas.