En capítulos anteriores trabajamos con funciones y tipos concretos: una función que suma enteros suma solo enteros, una que ordena strings ordena solo strings. Si queríamos la misma lógica para otro tipo, teníamos que reescribir la función. En este capítulo vamos a ver cómo escribir código que funcione para cualquier tipo usando tipos parametrizables (genéricos).
1El problema: lógica repetida para cada tipo¶
Supongamos que queremos una función que busque un elemento en un slice. Para enteros haríamos:
1 2 3 4 5 6 7 8func ContieneInt(arr []int, elem int) bool { for _, v := range arr { if v == elem { return true } } return false }
Si después necesitamos lo mismo para strings, escribimos otra función casi idéntica:
1 2 3 4 5 6 7 8func ContieneString(arr []string, elem string) bool { for _, v := range arr { if v == elem { return true } } return false }
La única diferencia es el tipo de los parámetros. El resto del código es exactamente igual. Esto viola el principio de no repetirse (DRY, por su sigla en inglés Don’t Repeat Yourself; Wikipedia) y hace que el código sea más difícil de mantener.
1.1Solución clásica: interface{}¶
Antes de que Go tuviera genéricos, la solución era usar el tipo vacío interface{} (que también puede escribirse como any). Como interface{} no impone ningún método, cualquier valor lo satisface:
1 2 3 4 5 6 7 8func ContieneAny(arr []interface{}, elem interface{}) bool { for _, v := range arr { if v == elem { return true } } return false }
Pero esta solución tiene problemas:
Sin verificación en compilación: si se intentan hacer operaciones como sumar dos valores de tipo
any, el código compila pero falla en ejecución. El compilador no ayuda a encontrar el error.Código verboso: para usar el valor hay que preguntar por su tipo concreto con
valor.(Tipo), y si el tipo no coincide, el programa explota con un panic.Pérdida de información: adentro del
interface{}el compilador no sabe si hay unint, unstringo lo que sea.
1 2 3 4 5func main() { nums := []interface{}{10, 20, 30} fmt.Println(ContieneAny(nums, 20)) // true, funciona bien fmt.Println(ContieneAny(nums, "hola")) // false, compila pero no tiene sentido }
true
falseEl problema es que el compilador no nos protege: pasar un string donde debería ir un int no genera ningún error en compilación. Si la función hiciera operaciones aritméticas, el error aparecería recién en ejecución.
2Tipos parametrizables (type parameters)¶
Desde Go 1.18, podemos escribir funciones que acepten cualquier tipo usando tipos parametrizables. La sintaxis usa corchetes []:
1 2 3 4 5 6 7 8func Contiene[T comparable](arr []T, elem T) bool { for _, v := range arr { if v == elem { return true } } return false }
Analicemos la sintaxis:
[T comparable]declara un tipo parametrizableTcon la restricción (constraint)comparable.Tse usa como tipo de los parámetrosarr(slice deT) yelem(T).Dentro de la función, los valores son del tipo concreto
T, nointerface{}..
La restricción comparable indica que T debe soportar los operadores == y !=, que son los que usamos dentro de la función.
Al llamar a la función, Go infiere el tipo automáticamente:
1 2 3 4 5 6 7 8func main() { numeros := []int{10, 20, 30, 40, 50} fmt.Println(Contiene(numeros, 30)) // true fmt.Println(Contiene(numeros, 99)) // false nombres := []string{"Ana", "Luis", "Pepe"} fmt.Println(Contiene(nombres, "Luis")) // true }
true
false
trueLa misma función Contiene funciona con int, string, y cualquier otro tipo que sea comparable. Go genera el código necesario para cada tipo concreto en tiempo de compilación, sin penalidad en ejecución.
A diferencia de la solución con interface{}, acá el compilador no permite mezclar tipos:
si declaramos un slice de enteros, solo podemos buscar enteros dentro de él:
1 2 3 4 5 6 7func main() { numeros := []int{10, 20, 30} fmt.Println(Contiene(numeros, 20)) // bien: int con int // Esto NO compila: // fmt.Println(Contiene(numeros, "hola")) // error: string no es int }
El error en compilación nos protege de equivocarnos antes de ejecutar el programa.
2.1Inferencia de tipos¶
Al llamar a una función genérica, el compilador deduce los tipos parametrizables a partir de los argumentos:
1 2Contiene(numeros, 30) // T se infiere como int Contiene(nombres, "Luis") // T se infiere como string
También podemos especificar el tipo explícitamente si hace falta:
1Contiene[int](numeros, 30)
Los tipos parametrizables permiten que una misma función trabaje con diferentes tipos concretos.
Los tipos parametrizables permiten que una misma función trabaje con diferentes tipos concretos.
3Restricciones (constraints)¶
Los constraints definen qué operaciones puede hacer T dentro de la función. Go provee algunos constraints predefinidos y permite crear los nuestros.
3.1any¶
any es un alias para interface{}. Acepta cualquier tipo, pero no permite ninguna operación específica sobre los valores (no se puede usar ==, <, +, etc.):
1 2 3 4 5func Imprimir[T any](arr []T) { for _, v := range arr { fmt.Println(v) } }
Esta función solo puede usar operaciones válidas para cualquier tipo: asignar, pasar como parámetro, imprimir con fmt.Println (que usa interface{} internamente).
3.2comparable¶
comparable restringe T a tipos que soporten == y !=. Todos los tipos básicos (enteros, strings, booleanos) son comparables, así como punteros, structs con campos comparables y arreglos.
1 2 3 4 5 6 7 8func BuscarLineal[T comparable](arr []T, elem T) int { for i, v := range arr { if v == elem { return i } } return -1 }
3.3Restricciones personalizadas (custom constraints)¶
Para operaciones como <, >, <=, >=, no existe un constraint predefinido en la biblioteca estándar (hasta Go 1.21). Podemos definir el nuestro propio:
1 2 3type Ordenable interface { ~int | ~float64 | ~string }
El operador ~ indica que el tipo subyacente debe ser int, float64 o string. Esto permite usar tanto los tipos literales (int, string) como tipos definidos a partir de ellos (por ejemplo, type Edad int).
Ahora podemos escribir funciones que usen operadores de comparación:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20func Maximo[T Ordenable](arr []T) T { max := arr[0] for _, v := range arr[1:] { if v > max { max = v } } return max } func main() { enteros := []int{10, 5, 8, 3, 12} fmt.Println(Maximo(enteros)) // 12 alturas := []float64{1.75, 1.80, 1.65, 1.90} fmt.Println(Maximo(alturas)) // 1.9 nombres := []string{"Ana", "Luis", "Pepe", "Beatriz"} fmt.Println(Maximo(nombres)) // Pepe (orden lexicográfico) }
12
1.9
Pepe4Alternativa: función comparadora¶
En lugar de definir un constraint personalizado, podemos pasar una función que compare elementos. Acá el tipo es [T any] porque no hacemos operaciones directamente sobre T: toda la lógica de comparación está dentro de la función menor, que recibe los T y decide el orden.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22func OrdenarSeleccion[T any](arr []T, menor func(T, T) bool) { n := len(arr) for i := 0; i < n-1; i++ { minIdx := i for j := i + 1; j < n; j++ { if menor(arr[j], arr[minIdx]) { minIdx = j } } arr[i], arr[minIdx] = arr[minIdx], arr[i] } } func main() { numeros := []int{4, 2, 7, 1, 9} OrdenarSeleccion(numeros, func(a, b int) bool { return a < b }) fmt.Println(numeros) // [1 2 4 7 9] nombres := []string{"Pepe", "Ana", "Luis"} OrdenarSeleccion(nombres, func(a, b string) bool { return a < b }) fmt.Println(nombres) // [Ana Luis Pepe] }
[1 2 4 7 9]
[Ana Luis Pepe]La función menor recibe dos elementos T y devuelve true si el primero debe ir antes que el segundo. Esto nos permite ordenar cualquier tipo, incluso structs sin un orden natural:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18type Persona struct { Nombre string Edad int } func main() { personas := []Persona{ {"Ana", 30}, {"Luis", 25}, {"Pepe", 35}, } OrdenarSeleccion(personas, func(a, b Persona) bool { return a.Edad < b.Edad }) fmt.Println(personas) // [{Luis 25} {Ana 30} {Pepe 35}] }
[{Luis 25} {Ana 30} {Pepe 35}]5Tipos genéricos¶
Los tipos parametrizables también funcionan con structs. Esto permite definir contenedores que almacenen valores de cualquier tipo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20type Caja[T any] struct { valor T } func (c *Caja[T]) Set(valor T) { c.valor = valor } func (c *Caja[T]) Get() T { return c.valor } func main() { cajaInt := Caja[int]{} cajaInt.Set(42) fmt.Println(cajaInt.Get()) // 42 cajaStr := Caja[string]{valor: "hola"} fmt.Println(cajaStr.Get()) // hola }
42
holaUn tipo genérico también puede tener múltiples parámetros:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16type Dicc[K comparable, V any] struct { datos map[K]V } func NuevoDicc[K comparable, V any]() *Dicc[K, V] { return &Dicc[K, V]{datos: make(map[K]V)} } func (d *Dicc[K, V]) Set(clave K, valor V) { d.datos[clave] = valor } func (d *Dicc[K, V]) Get(clave K) (V, bool) { v, ok := d.datos[clave] return v, ok }
6Resumen¶
| Concepto | Sintaxis | Ejemplo |
|---|---|---|
| Parámetro de tipo | [T any] | func Imprimir[T any](arr []T) |
Constraint comparable | [T comparable] | func Contiene[T comparable](arr []T, elem T) bool |
| Constraint personalizado | type X interface { ~int | ~string } | func Maximo[T Ordenable](arr []T) T |
| Función comparadora | Parámetro func(T, T) bool | func Ordenar[T any](arr []T, menor func(T, T) bool) |
| Struct genérico | type Nombre[T any] struct { ... } | type Caja[T any] struct { valor T } |
| Múltiples parámetros | [K comparable, V any] | type Dicc[K comparable, V any] struct { ... } |
7Ejercicios¶
Los ejercicios de este capítulo están en 12-genericos/ejercicios/
del repositorio taller-go.
Cada directorio contiene un README.md con el enunciado y los esqueletos
para resolverlo.