Los generics [disponible desde Go 1.18] permiten definir funciones y tipos con parámetros de tipo, permitiendo que el código sea reutilizable para diferentes tipos de datos manteniendo la seguridad de tipos en tiempo de compilación. Su propósito fundamental es permitir la abstracción sobre tipos, de modo que el compilador pueda generar la implementación específica necesaria para cada caso. La lógica interna de Go está diseñada para que, en la mayoría de los escenarios, el costo de rendimiento sea despreciable, utilizando diccionarios de tipos para manejar la resolución de las implementaciones.
Debes usar generics únicamente cuando la lógica del algoritmo sea idéntica independientemente del tipo de dato que esté procesando; es decir, cuando el código trata a los elementos de una colección de forma uniforme. Un caso de uso clásico es el procesamiento de colecciones: funciones para filtrar, mapear, buscar o transformar elementos en un slice o un map. Sin embargo, si el comportamiento de la función depende de las capacidades específicas de cada tipo (por ejemplo, cómo se calcula un interés o cómo se formatea un nombre), lo que necesitas es una interface.
Si intentas usar generics para resolver problemas de comportamiento en lugar de problemas de estructura, terminarás con una “sopa de tipos” (type parameters excesivos y restricciones complejas) que hace el código casi imposible de leer. El error crítico es la generalización prematura: intentar escribir una función genérica para un problema que solo ocurre una vez. Si no tienes al menos dos instancias de un mismo patrón de código idéntico, mantente con código concreto. Si te equivocas y generalizas de más, lo que terminas es añadiendo complejidad cognitiva sin haber eliminado una duplicación real.
package main
import "fmt"
// Contains es el caso de uso ideal para generics: la lógica de comparación
// (usar ==) es la misma para cualquier tipo que cumpla con la restricción
// 'comparable'. No importa si es un int o un string.
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}
// Filter demuestra el uso de 'any' porque la lógica de comparación
// no reside en el tipo T, sino en la función 'predicate' que el usuario provee.
func Filter[T any](slice []T, predicate func(T) bool) []T {
result := make([]T, 0, len(slice)) // Pre-alocamos capacidad para eficiencia
for _, v := range slice {
if predicate(v) {
result = append(result, v)
}
}
return result
}
// En este caso, NO usamos generics para el comportamiento.
// Si queremos que algo se pueda imprimir de una forma especial,
// definimos un contrato mediante una interface.
type Formateable interface {
Format() string
}
type Usuario struct {
ID int
Nombre string
}
func (u Usuario) Format() string {
return fmt.Sprintf("ID: %d | Nombre: %s", u.ID, u.Nombre)
}
// PrintList usa una interface para permitir polimorfismo de comportamiento.
// No nos importa el tipo concreto, sino que cumpla con el contrato Format.
func PrintList[T Formateable](items []T) {
for _, item := range items {
fmt.Println(item.Format())
}
}
func main() {
// Ejemplo 1: Generics para colecciones (Tipos diferentes, misma lógica)
nums := []int{10, 20, 30, 40, 50}
fmt.Println("¿Contiene 30?", Contains(nums, 30))
words := []string{"go", "generics", "pragmatism"}
fmt.Println("¿Contiene 'rust'?", Contains(words, "rust"))
// Ejemplo 2: Generics para transformación
pares := Filter(nums, func(n int) bool { return n%2 == 0 })
fmt.Println("Números pares:", pares)
// Ejemplo 3: Interfaces para comportamiento (Polimorfismo)
usuarios := []Usuario{
{ID: 1, Nombre: "Alice"},
{ID: 2, Nombre: "Bob"},
}
PrintList(usuarios)
}
Análisis del código
Fíjate en Contains[T comparable]. Aquí estamos obligados a usar la restricción comparable porque dentro de la función intentamos usar el operador ==. Si usáramos any, el compilador rechazaría el código porque no todos los tipos en Go pueden compararse con ==.
En Filter[T any], el parámetro T es any porque la función no necesita saber qué hace con el elemento; la responsabilidad de la lógica de comparación se delega a la función anónima predicate. Esta es la esencia de la utilidad de los generics en colecciones: separar la iteración (que es siempre igual) de la lógica de decisión.
Observa la diferencia con PrintList[T Formateable]. Aunque técnicamente es una función genérica, su propósito real no es evitar la duplicación de código de iteración, sino asegurar que cualquier tipo pasado tenga el método Format(). Si tu objetivo es que dos tipos diferentes hagan cosas distintas (como un Usuario que imprime su nombre y un Producto que imprime su SKU), no busques un generic que “sirva para todo”; define una interfaz que describa el comportamiento que esperas.
El uso de make([]T, 0, len(slice)) en Filter es una optimización de memoria que aprovecha que ya conocemos el tamaño máximo posible, evitando re-alocaciones innecesarias mientras el slice crece.
El error frecuente
El error más común es intentar “generalizar el comportamiento” en lugar de “generalizar la estructura”.
// ERROR: Intentar crear una función genérica para sumar cosas que no son iguales.
// Aunque parezca que "sumar" es una operación común, los tipos int y float64
// requieren instrucciones de CPU distintas y no comparten una interfaz de "sumable"
// en el lenguaje.
func SumarErroneo[T any](a, b T) T {
return a + b // Error de compilación: el operador + no está definido para 'any'
}
Si intentas forzar una solución genérica para algo que requiere lógica distinta según el tipo, terminarás escribiendo un switch gigante de tipos dentro de la función genérica. En ese momento, has perdido toda la ventaja de los generics y has creado un código mucho más complejo y difícil de mantener que si hubieras escrito dos funciones concretas o usado interfaces.
N° 86