Introducción: ¿qué son los Generics y por qué aparecieron en Go?
Durante mucho tiempo, la comunidad de Go (Golang) prescindió de los genéricos (programación genérica). Los desarrolladores usaban interfaces, interface{} vacíos y generación de código para sortear las limitaciones del tipado estricto. En la versión Go 1.18 (febrero de 2022), finalmente se agregaron al lenguaje los genéricos, una herramienta poderosa que permite escribir funciones y estructuras que trabajan con diferentes tipos de datos sin perder la seguridad de tipos.
En este artículo, analizaremos para qué sirven los Generics en Go, cómo funcionan y aprenderemos a aplicarlos en la práctica. El material está dirigido a desarrolladores principiantes que ya conocen los fundamentos de Go: variables, funciones y slices.
¿Para qué sirven los genéricos en Go?
Imagina que necesitas escribir una función que encuentre el valor mínimo en un slice de números. Sin genéricos, tendrías que escribir funciones separadas para int, float64 y otros tipos:
// Sin genéricos — tres funciones idénticasfunc MinInt(a, b int) int { if a < b { return a } return b}
func MinFloat64(a, b float64) float64 { if a < b { return a } return b}Esto provoca duplicación de código. Antes, este problema se resolvía mediante interface{} y conversión de tipos, pero este enfoque perdía seguridad en la etapa de compilación:
func Min(a, b interface{}) interface{} { // Comparación y conversión — fácil equivocarse return a}Los genéricos resuelven tres problemas clave:
- Seguridad de tipos — los errores de tipo se detectan en tiempo de compilación.
- Eliminación de duplicación — una función funciona con cualquier tipo.
- Legibilidad del código — indicación explícita de qué tipos son compatibles.
Fundamentos de sintaxis: type parameters y constraints
Los genéricos en Go se basan en dos conceptos: parámetros de tipo (type parameters) y restricciones (constraints).
Parámetros de tipo
Los parámetros de tipo se indican entre corchetes después del nombre de la función o tipo. Por convención, se suele usar la letra T (de Type), pero se puede usar cualquier nombre:
// Función con un parámetro de tipo Tfunc Print[T any](value T) { fmt.Println(value)}
// UsoPrint[int](42) // indicamos el tipo explícitamentePrint("Hello") // el compilador deduce el tipo automáticamenteLa palabra clave any es un alias para la interfaz vacía interface{}. Significa que la función acepta cualquier tipo.
Restricciones (constraints)
Las restricciones definen qué tipos pueden usarse con un genérico. Por ejemplo, para comparar números, el tipo debe soportar los operadores < y >. Las restricciones integradas para tipos numéricos se encuentran en el paquete golang.org/x/exp/constraints:
import "golang.org/x/exp/constraints"
// Restricción — solo números enterosfunc Sum[T constraints.Integer](numbers []T) T { var total T for _, n := range numbers { total += n } return total}También puedes crear tus propias restricciones mediante interfaces:
// Restricción propia: el tipo debe tener un método String()type Stringer interface { String() string}
func Describe[T Stringer](value T) string { return "Value: " + value.String()}Ejemplos prácticos de uso de Generics
Veamos varios escenarios reales donde los genéricos simplifican significativamente el código.
1. Función universal de búsqueda en un slice
// Encuentra el índice de un elemento en un slice. -1 si no se encuentra.func Index[T comparable](slice []T, target T