Generics en Go: tres años después, qué ha sobrevivido
Índice de contenidos
- Puntos clave
- Lo que no ha ocurrido
- Lo que sí ha pasado
- Lo que ha sorprendido
- Lo que no ha cuajado
- Mirando a dónde va
- Conclusión
- Preguntas frecuentes
- ¿Cuándo debería usar generics en mi código Go en lugar de una función concreta?
- ¿Puedo declarar un método genérico como `coll.Map(fn)` sobre una colección en Go?
- ¿Qué ventaja práctica dan los generics al trabajar con bases de datos en Go?
- Fuentes
Los generics llegaron a Go en marzo de 2022 con mucha expectación y algo de recelo. Tres años después, el código idiomático apenas los usa en la superficie, pero han transformado las bibliotecas profundas: colecciones tipo-seguras, clientes de base de datos, primitivas de concurrencia. Este análisis examina qué ha cuajado y por qué.
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 concreto en el lenguaje, y entender cuál es ese sitio dice bastante sobre cómo ha evolucionado el pensamiento de la comunidad.
Puntos clave
-
Los generics no sustituyeron a
interface{}en la superficie del código idiomático; la predicción más común fue incorrecta. -
Donde han arraigado con fuerza es en bibliotecas de bajo nivel: colecciones, primitivas de concurrencia, clientes de base de datos.
-
La biblioteca
slices[1] (promovida a estándar en Go 1.21) es el caso de uso diario más tangible. -
Los métodos genéricos en receptores no genéricos siguen sin permitirse; la comunidad los reemplaza con funciones libres.
-
La regla que mejor ha aguantado: escribe la función concreta primero; generaliza solo si copias la misma lógica para tres tipos distintos.
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. Mira el código idiomático de los proyectos Go más activos: Kubernetes, Docker, CockroachDB, las propias bibliotecas 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 está en la forma que finalmente se aprobó para Go. Los generics 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 los 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 bibliotecas. 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[2], 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 se cumplen tres condiciones:
-
La lógica es realmente idéntica para distintos tipos, no solo similar.
-
Hay un rendimiento que perder con
anyy 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.
El caso de slices es ilustrativo. Esta biblioteca 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 no anticipado es el impacto de los generics 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 la serialización JSON, al parseo de configuración y a los 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[3] 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. Esta evolución del tipado fuerte en Go conecta con los cambios de PEP 695 que analizamos en Python 3.12.
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 bibliotecas probadas con décadas de uso.
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 donde aportan valor real: bibliotecas de bajo nivel, utilidades de colecciones, clientes de datos. Se han mantenido fuera de 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. Uno es permitir más inferencia de tipos genéricos donde hoy hay que anotarlos explícitamente. Otro, quizá, permitir métodos genéricos en circunstancias limitadas. Nada de esto rompe el modelo actual, solo lo pule.
Conclusión
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.
Los generics en Go son una herramienta de infraestructura de bibliotecas, no una herramienta de código de aplicación. Entender ese sitio específico, ni más grande ni más pequeño, es lo que hace que su adopción sea exitosa.
Fuentes:
- Go Blog: An Introduction To Generics[4]
- pkg.go.dev: biblioteca slices (Go 1.21+)[1]
- GitHub: sourcegraph/conc, concurrencia estructurada para Go[3]
Preguntas frecuentes
¿Cuándo debería usar generics en mi código Go en lugar de una función concreta?
Escribe primero la función concreta y generaliza solo si te encuentras copiándola para tres tipos distintos con lógica idéntica. El patrón que ha emergido exige tres condiciones: la lógica es realmente idéntica y no solo similar. Hay un rendimiento que se pierde con any y reflection; por último, la firma genérica es más clara para el usuario que una interfaz. Fuera de esos casos, lo idiomático en Go sigue siendo la función concreta, y los generics quedan como herramienta de infraestructura de bibliotecas, no de código de aplicación.
¿Puedo declarar un método genérico como `coll.Map(fn)` sobre una colección en Go?
No. Go no permite métodos genéricos en un receptor no genérico, porque la constraint está en el tipo completo, lo que impide patrones tipo método Map en una colección que otros lenguajes sí permiten. La comunidad ha aprendido a vivir con ello mediante funciones libres, Map(coll, fn) en lugar de coll.Map(fn). Entre las propuestas en debate está permitir métodos genéricos en circunstancias limitadas y más inferencia de tipos donde hoy hay que anotarlos explícitamente.
¿Qué ventaja práctica dan los generics al trabajar con bases de datos en Go?
Escaneo tipo-seguro de filas a structs sin reflection ni ceremonia. pgx en su versión 5 ofrece pgx.CollectRows[T], que devuelve directamente un []T y hace que el compilador avise si la estructura no coincide con las columnas; sqlc sigue el mismo camino. El patrón se ha extendido a serialización JSON, parseo de configuración y frameworks de testing. Ahí es donde los generics más aportan: conservas la verificación de tipos a través de una abstracción que antes la perdía por completo.