Generics en Go: cuándo usarlos y cuándo no en 2026

Go salió en 2009 y durante más de una década no tuvo generics. La pregunta más repetida en la comunidad era siempre la misma: ¿cuándo? La respuesta del equipo de Google fue constante: cuando lo hagamos bien.

El problema no era de voluntad sino de diseño. Los generics de Java y C++ arrastraban una complejidad que el equipo de Go quería evitar. Java usa type erasure, lo que significa que en tiempo de ejecución se pierde la información del tipo. C++ genera código especializado para cada tipo, lo que infla el binario. Ninguno de los dos enfoques encajaba con la filosofía de Go: sencillez, compilación rápida, binarios pequeños.

La propuesta que finalmente llegó a Go 1.18 en marzo de 2022 tomó un camino diferente: type parameters con constraints. Más adelante lo explico en detalle, pero la clave es que las constraints son interfaces, algo que Go ya tenía. No se inventó una nueva abstracción, se extendió una existente.

Desde Go 1.21 los generics dejaron de ser una curiosidad del lenguaje para convertirse en parte de la biblioteca estándar. Los paquetes slices, maps y cmp los usan de forma intensiva, y probablemente ya los estás usando sin darte cuenta si trabajas con Go moderno.

Sintaxis básica: type parameters

La forma más directa de ver cómo funcionan los generics es con un ejemplo sencillo. Una función que devuelve el mínimo de dos valores:

func Min[T cmp.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

Hay tres partes que no verías en una función normal. El [T cmp.Ordered] declara el type parameter: T es el nombre del tipo genérico y cmp.Ordered es la constraint que limita qué tipos puede ser T. Después los parámetros a, b T usan ese tipo genérico, y el tipo de retorno también es T.

Para llamar a esta función no tienes que hacer nada especial:

resultado := Min(3, 5)        // Go infiere T=int
nombre := Min("alfa", "beta") // Go infiere T=string

El compilador deduce el tipo a partir de los argumentos. En la mayoría de casos no tendrás que escribir Min[int](3, 5) de forma explícita.

Constraints: cómo limitar los tipos que acepta tu función

Una constraint es simplemente una interfaz que describe qué operaciones puede hacer el tipo genérico. Si tu función necesita comparar con <, la constraint tiene que garantizar que el tipo soporta ese operador.

La biblioteca estándar trae varias constraints listas para usar:

  • cmp.Ordered: todos los tipos que soportan <, >, <= y >=. Incluye todos los enteros, flotantes y strings.
  • comparable: todos los tipos que soportan == y !=. Más amplio que Ordered porque incluye structs, punteros y otros tipos que no tienen orden natural.

También puedes definir tus propias constraints:

type Number interface {
    int | int64 | float64
}

Con esta constraint puedes escribir una función que acepta exactamente esos tres tipos numéricos y ninguno más. La sintaxis de unión de tipos (int | int64 | float64) es nueva en Go 1.18 y solo tiene sentido dentro de una constraint.

La stdlib con generics: slices y maps

Antes de Go 1.21, si querías buscar un elemento en una slice o sacar el máximo de una lista de números tenías dos opciones: escribirte la función a mano o tirar de golang.org/x/exp/slices, el paquete experimental. Desde 1.21 esas funciones están en la biblioteca estándar, escritas con generics.

Las más útiles del paquete slices:

  • slices.Contains(s, v): devuelve true si la slice contiene el valor v.
  • slices.Max(s): devuelve el elemento máximo. Funciona con cualquier tipo Ordered.
  • slices.SortFunc(s, func(a, b T) int): ordena con una función de comparación personalizada.
  • slices.Index(s, v): devuelve el índice de la primera ocurrencia de v, o -1 si no está.

Y del paquete maps:

  • maps.Keys(m): devuelve las claves del mapa como slice. El orden no está garantizado.
  • maps.Values(m): devuelve los valores.
  • maps.Copy(dst, src): copia todas las entradas de src en dst.

Un ejemplo concreto con slices:

import "slices"

nombres := []string{"Carlos", "Ana", "Luis", "Beatriz"}
slices.Sort(nombres)
fmt.Println(slices.Contains(nombres, "Ana")) // true
fmt.Println(slices.Max(nombres))             // Luis (alfabéticamente último)

Antes de 1.21 esto requería un bucle a mano o una dependencia externa. Ahora está en la stdlib y funciona con cualquier tipo que cumpla la constraint correspondiente.

Cuándo sí tiene sentido usar generics

Los generics resuelven bien un problema concreto: cuando tienes lógica que es idéntica para varios tipos y sin generics tendrías que copiar código o renunciar a la seguridad de tipos usando interface{}.

Los casos donde sí aportan:

  • Estructuras de datos genéricas. Una pila, una cola o un conjunto que funcione con cualquier tipo. Sin generics o la duplicas para cada tipo o pierdes la comprobación en compilación.
  • Funciones de utilidad sobre slices o maps. Filtrar, mapear, reducir. Son las candidatas más claras.
  • Librerías. Si estás escribiendo una librería que otros van a usar, los generics hacen la API más segura sin obligar al usuario a hacer type assertions.

Una pila genérica sencilla:

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
    if len(s.items) == 0 {
        return zero, false
    }
    n := len(s.items) - 1
    v := s.items[n]
    s.items = s.items[:n]
    return v, true
}

Puedes usar esta misma implementación para Stack[int], Stack[string] o Stack[MiStruct] sin escribir ni una línea más.

Si quieres profundizar en cómo encajan los generics con el estado de Go y sus nuevas funcionalidades, el artículo enlazado cubre el panorama general del lenguaje en 2025.

Cuándo no usar generics

Aquí está la parte que menos se menciona en los tutoriales: los generics también se pueden usar mal, y en Go se abusa de ellos con más frecuencia de la que parece.

  • Cuando any ya funciona. Si tu función recibe un valor, lo almacena y lo devuelve sin hacer nada con él, any (alias de interface{}) es suficiente. Añadir un type parameter no da más seguridad en ese caso.
  • Cuando tienes dos o tres tipos concretos. Si necesitas la misma función para int y string, dos funciones separadas suelen ser más legibles. Los generics añaden una capa de abstracción que el lector tiene que deshacer mentalmente.
  • En lógica de negocio. Las funciones de dominio raramente son genéricas de verdad. Un CalcularDescuento[T Precio] casi siempre es un error de diseño, no una mejora.
  • Cuando el código no lo pide. Si escribes un type parameter y luego lo usas en un solo lugar, probablemente lo que necesitas es una función normal.

El equipo de Go lo dice explícitamente en su FAQ: si no estás seguro de si usar generics, probablemente no los necesites. Go tiene una cultura de legibilidad que vale la pena preservar.

Type inference: Go deduce el tipo por ti

Uno de los puntos más cómodos de la implementación de Go es que el compilador infiere el tipo genérico en la mayoría de situaciones. No tienes que escribirlo:

nums := []int{3, 1, 4, 1, 5, 9}
maximo := slices.Max(nums) // Go infiere T=int, no hace falta slices.Max[int](nums)

La inferencia funciona mirando los argumentos de la llamada. Si el compilador puede determinar T sin ambigüedad, lo hace solo. Solo tienes que especificarlo cuando la inferencia falla, y eso suele ocurrir cuando el tipo genérico aparece solo en el valor de retorno y no en los parámetros:

// Aquí Go no puede inferir T porque no hay argumentos que lo determinen
result := MakeSlice[int](10)

En la práctica, la especificación explícita del tipo es rara en código de aplicación. En código de librería aparece algo más.

Limitaciones que conviene conocer

Los generics de Go no son los de C++ ni los de Rust. Hay cosas que no puedes hacer y que en otros lenguajes sí están disponibles.

No hay specialization. En C++ puedes dar una implementación distinta de una función para un tipo concreto. En Go no. Si T es int o string tu función hace exactamente lo mismo en ambos casos. Si necesitas comportamiento diferente por tipo, usa interfaces o funciones separadas.

No hay generic methods. Puedes tener un struct genérico y sus métodos usan el type parameter del struct, pero no puedes añadir un type parameter nuevo en el propio método:

// Esto sí funciona: el método usa el T del struct
func (s *Stack[T]) Push(v T) { ... }

// Esto NO compila: Go no permite type parameters adicionales en métodos
func (s *Stack[T]) Convert[U any]() Stack[U] { ... }

Las constraints no cubren todos los operadores. Si quieres usar + en un tipo genérico no basta con comparable. Necesitas una constraint que incluya explícitamente los tipos que soportan la suma, o usar el tipo cmp.Ordered si el operador está cubierto.

Si te interesa comparar estas limitaciones con las de otros lenguajes, el artículo sobre generics en Go comparados con los de Rust entra en detalle en las diferencias de diseño.

Un resumen práctico

Los generics de Go son una herramienta útil cuando los necesitas, no un sustituto de interfaces ni una forma de hacer el código más sofisticado. La biblioteca estándar los usa para las funciones de utilidad más comunes sobre slices y maps, que es exactamente el caso de uso más claro.

Antes de añadir un type parameter a una función, hazte una pregunta sencilla: ¿este código sería prácticamente idéntico para dos o más tipos distintos? Si la respuesta es sí, los generics tienen sentido. Si la respuesta es «más o menos» o «puede que en el futuro», mejor deja la función sin generics por ahora. Go no penaliza el código sin generics.

Imagen: Pexels / Miguel Á. Padriñán

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP