Cuando Go 1.18 añadió generics en marzo de 2022, la comunidad se dividió en dos campos. Unos llevaban años pidiéndolos y los recibieron como el paso que faltaba para modernizar el lenguaje. Otros sospechaban que iban a corromper la simplicidad que hizo a Go atractivo. Ambos tenían parte de razón, y tres años después tenemos perspectiva suficiente para juzgarlo sin la pasión del momento inicial.
Lo más interesante es que el resultado no ha ido exactamente en ninguna de las dos direcciones previstas. Los generics en Go no han explotado en uso masivo, pero tampoco han sido una patata caliente ignorada. Ocupan un sitio muy concreto en el lenguaje, y entender cuál es ese sitio dice bastante sobre cómo ha evolucionado el pensamiento de la comunidad.
Lo que no ha ocurrido
La predicción más común al principio era que los generics iban a sustituir a interface{} en todos los contextos donde este se usaba. Lo cierto es que eso no ha pasado. Si miras el código idiomático de los proyectos Go más activos (Kubernetes, Docker, CockroachDB, las propias librerías estándar), la superficie pública sigue dominada por interfaces tradicionales, funciones con firmas concretas, y uso de any donde la flexibilidad es realmente necesaria.
La razón principal es que los generics, en la forma que finalmente se aprobó para Go, tienen restricciones de diseño que los hacen menos universales que los templates de C++ o los trait bounds de Rust. No se puede hacer cualquier cosa: las operaciones que se pueden aplicar a un tipo genérico están limitadas por las interfaces de constraint que declaras. Esto es una virtud de diseño, porque mantiene los tiempos de compilación razonables y evita el tipo de mensajes de error incomprensibles que C++ produce, pero tiene un coste de expresividad.
La segunda razón es cultural. La filosofía de Go siempre ha preferido la concreción a la abstracción, y la comunidad ha internalizado eso lo suficiente como para que el reflejo por defecto sea «escribe la función concreta primero, generaliza solo si se demuestra necesario». La existencia de generics no ha cambiado ese reflejo.
Lo que sí ha pasado
Donde los generics han arraigado con fuerza es en las capas profundas de las librerías. Si abres código de golang.org/x/exp/slices, de las utilidades de sync en versiones recientes, o de clientes de base de datos como pgx, encontrarás generics por todas partes. Son el tipo de código que tiene sentido generalizar: operaciones fundamentales sobre colecciones, utilidades de concurrencia, deserializadores.
El patrón que ha emergido es claro. Las funciones genéricas se usan cuando: – La lógica es realmente idéntica para distintos tipos, no solo similar. – Hay un rendimiento que perder con any y reflection, y el coste se nota. – La firma genérica es más clara para el usuario que una interfaz.
Fuera de esos casos, lo idiomático sigue siendo escribir la función concreta, y ahí se nota una resistencia saludable al patrón «todo genérico por si acaso».
El caso de slices es ilustrativo. Esta librería estaba experimentalmente disponible desde hace tiempo y se promovió a la estándar en Go 1.21. Ofrece funciones como slices.Contains, slices.Index, slices.Sort, implementadas con generics. Cualquiera que hubiera escrito Go antes de 2022 había reimplementado estas funciones una y otra vez para cada tipo, porque no había alternativa razonable. Ahora son una línea, y ese es un beneficio real y cotidiano.
Lo que ha sorprendido
Un efecto que no anticipé es el impacto que han tenido en los clientes de base de datos. sqlc y, sobre todo, pgx en su versión 5 ofrecen escaneo tipo-seguro de filas a structs mediante generics. Antes, esto se hacía con reflection y una buena cantidad de ceremonia; ahora tienes una función pgx.CollectRows[T] que devuelve un []T directamente, y el compilador te avisa si la estructura no coincide con las columnas.
Este patrón se ha extendido a serialización JSON, a parseo de configuración y a varios frameworks de testing. Es el escenario donde los generics más añaden: conservas la verificación de tipos a través de una abstracción que antes la perdía por completo.
El otro sitio donde los generics han cuajado inesperadamente es en los canales tipados y las primitivas de concurrencia. Bibliotecas como sourcegraph/conc ofrecen pools de trabajadores, grupos de resultados y primitivas similares con generics, y la experiencia es cualitativamente mejor que el equivalente con interface{} y type assertions.
Lo que no ha cuajado
La gran excepción es lo que algunos esperaban que fuera el caso estrella: los tipos de datos complejos genéricos, como árboles balanceados, heaps tipados, grafos. Esos existen en el ecosistema, pero su adopción ha sido moderada. La razón es simple: para colecciones de tamaño moderado, los slices y maps nativos son suficientes, y para casos que exigen estructuras especializadas, los desarrolladores prefieren librerías probadas con décadas de uso, que existen en su forma no genérica y funcionan bien con type assertions puntuales.
Los métodos genéricos son otra historia. Go no permite declarar métodos genéricos en un receptor no genérico (la constraint está en el tipo completo). Esto limita los patrones tipo «método Map en una colección» que otros lenguajes permiten. La comunidad ha aprendido a vivir con eso mediante funciones libres (Map(coll, fn) en lugar de coll.Map(fn)), pero es una rugosidad que probablemente se suavice en versiones futuras.
Mirando a dónde va
Tres años después, mi lectura es que Go ha absorbido los generics sin perder identidad. Han entrado en el sitio donde aportan valor real (librerías de bajo nivel, utilidades de colecciones, clientes de datos) y se han mantenido fuera del sitio donde habrían introducido ruido (código de negocio cotidiano, APIs públicas de servicios). Eso es bastante sano.
Lo que viene, según las propuestas que se están debatiendo, es flexibilizar algunos casos concretos: permitir más inferencia de tipos genéricos en casos donde hoy hay que anotarlos explícitamente, y quizá permitir métodos genéricos en algunas circunstancias limitadas. Nada de esto rompe el modelo actual, solo lo pule.
Si alguien me pregunta si debería empezar a usar generics en su código Go, mi respuesta sigue siendo la de hace tres años: primero escribe la función concreta. Si luego te encuentras copiándola para tres tipos distintos y la lógica es idéntica, entonces sí, generalízala. Ese criterio modesto es lo que mejor ha aguantado.