Una función genérica
Los parámetros de tipo van entre corchetes antes de los parámetros normales. Cada uno tiene una restricción que dice qué tipos se permiten.
T y U se infieren de los argumentos, así que rara vez escribes Map[int, string](...). La inferencia funciona a partir de los argumentos de la función; no puede inferir un tipo que solo aparece en el resultado. Para func Zero[T any]() T, tienes que escribir Zero[int]().
Antes de Go 1.18 esta función había que escribirla una vez por tipo, o recibir []interface{} y perder la seguridad de tipos.
Restricciones
Una restricción es una interfaz. Limita qué tipos se pueden usar y, a cambio, le dice al compilador qué operaciones se permiten sobre los valores de ese tipo.
| Restricción | Admite | Te permite usar |
|---|---|---|
any | cualquier tipo | asignar, pasar, guardar |
comparable | tipos que admiten == | ==, !=, claves de map |
cmp.Ordered (Go 1.21) | enteros, flotantes, strings | <, <=, >, >=, además de == |
| una interfaz con métodos | tipos con esos métodos | llamar a los métodos |
una unión de tipos como ~int | ~float64 | exactamente esos tipos | los operadores que admiten todos ellos |
Con any en lugar de comparable, v == target no compila: invalid operation: v == target (incomparable types in type set). La restricción es lo que hace legal el operador.
Estas dos funciones auxiliares ya existen: slices.Index y slices.Max. Los paquetes estándar slices, maps y cmp cubren la mayoría de las necesidades genéricas del día a día, así que revísalos antes de escribir las tuyas.
Restricciones propias con uniones de tipos y ~
Una restricción puede enumerar tipos con |. Los operadores permitidos son los que admiten todos los tipos enumerados.
La tilde importa. ~int64 significa "cualquier tipo cuyo tipo subyacente sea int64", así que se acepta el tipo con nombre Cents. Escrito como int64 a secas, Sum([]Cents{...}) falla con Cents does not satisfy Number (possibly missing ~ for int64 in Number).
Las interfaces que contienen uniones de tipos solo se pueden usar como restricciones, nunca como tipos normales de variable. var n Number no compila.
El paquete golang.org/x/exp/constraints tiene restricciones ya hechas como Integer, Float y Signed. Está fuera de la librería estándar, así que el entorno de ejecución de aquí no puede importarlo; cmp.Ordered es la estándar.
Tipos genéricos
Los structs, slices, maps y otros tipos también pueden tener parámetros de tipo. Los métodos de un tipo genérico usan los parámetros del receptor.
Dos detalles de este ejemplo aparecen constantemente:
- El valor cero de T.
var zero Tes la forma de devolver "nada" desde código genérico. No hay unT{}ni unnilque funcione para todos los tipos. - Instanciación. Un tipo genérico tiene que instanciarse antes de usarlo:
Stack[string], noStack. Dentro de sus propios métodos, el receptor se escribeStack[T].
Go 1.24 añadió los alias de tipo genéricos: type Set[T comparable] = map[T]struct{} ahora compila.
Límites con los que te vas a encontrar
- Los métodos no tienen parámetros de tipo propios.
func (s *Stack[T]) Map[U any](...)no está permitido. Usa una función de nivel superior. - Sin especialización. No puedes escribir una implementación aparte para
T = string. Un type switch sobreany(v)dentro de la función es el apaño, y normalmente una señal de que los genéricos no son la herramienta adecuada. - Sin restricciones de operadores por nombre. No puedes decir "cualquier tipo con un método
+"; enumeras los tipos en una unión. - El acceso a campos a través de una restricción no funciona. Aunque todos los tipos de una unión tengan un campo
ID,v.IDno está permitido. Pon un método en la restricción.
Cuándo no usar genéricos
Los genéricos encajan en tipos contenedor (pilas, conjuntos, cachés, árboles) y en algoritmos sobre slices y maps que hacen lo mismo para cualquier tipo de elemento. No encajan en todas partes.
- Un solo tipo concreto. Si la función solo se llama con
[]User, una versión genérica añade coste de lectura y nada más. - Comportamiento que cambia según el tipo. Para eso están las interfaces. Una función que recibe un
io.Writeres más sencilla que una que recibe[T io.Writer], y funciona igual. - Sustituir parámetros de interfaz.
func Print[T fmt.Stringer](v T)no es mejor quefunc Print(v fmt.Stringer).
La pauta del propio equipo de Go: escribe primero el código para un tipo concreto, y recurre a los parámetros de tipo cuando te veas escribiendo el mismo código por segunda vez cambiando solo los tipos.
Errores comunes
- Usar
anydonde hace faltacomparableocmp.Ordered. El compilador rechaza==o<sobre un parámetro de tipoany. - Olvidar
~. Los tipos definidos por el usuario comotype ID intno cumplen una restriccióninta secas. - Devolver
nilpara un parámetro de tipo. No está permitido salvo que la restricción limite T a tipos de tipo puntero. Devuelve un valor cero. - Escribir una función auxiliar genérica que ya existe. Revisa antes
slices,mapsycmp.
Preguntas frecuentes
¿Tiene Go genéricos?
Sí, desde Go 1.18 (marzo de 2022). Las funciones y los tipos pueden declarar parámetros de tipo entre corchetes: func Map[T, U any](s []T, f func(T) U) []U. Cada parámetro de tipo tiene una restricción, que es una interfaz que describe lo que el tipo debe admitir.
¿Qué diferencia hay entre any y comparable en los genéricos de Go?
any admite cualquier tipo, pero apenas te deja hacer nada con los valores salvo asignarlos, pasarlos y guardarlos. comparable admite solo los tipos que soportan == y !=, que es lo que necesitas para claves de map o comprobaciones de igualdad. Para < y >, usa cmp.Ordered.
¿Qué significa la tilde (~) en una restricción de Go?
~T significa "cualquier tipo cuyo tipo subyacente sea T". ~int coincide con int y también con type Celsius int. Sin la tilde, int en una restricción coincide solo con el propio int, así que los tipos definidos por el usuario se rechazarían.
¿Pueden los métodos de Go tener parámetros de tipo?
No. Un método puede usar los parámetros de tipo de su tipo receptor (func (s *Stack[T]) Push(v T)), pero no puede declarar otros nuevos. Escribe en su lugar una función genérica de nivel superior: func Map[T, U any](s *Stack[T], f func(T) U) *Stack[U].