Cuando escribes una función genérica, el tipo T no es solo un marcador de posición; es una promesa que le haces al compilador. Si declaras [T any], le estás diciendo que T puede ser absolutamente cualquier tipo, pero eso te limita: dentro de la función, solo podrás tratar a T como un interface{} (un contenedor de datos sin comportamiento conocido). Si intentas usar un operador como > o == con un tipo any, el compilador se detendrá porque no puede garantizar que todos los tipos posibles soporten esa operación.
Las constraints (restricciones) son las reglas que definen qué operaciones está permitido realizar sobre un tipo genérico. Funcionan mediante interfaces que agrupan tipos permitidos, permitiendo al compilador verificar la validez de las operaciones en tiempo de compilación en lugar de fallar en tiempo de ejecución. Debes usarlas cuando necesites realizar acciones específicas: usa any para contenedores puros, comparable si necesitas comparar igualdad (fundamental para usar tipos como clave en un map), y la restricción de ordenación (como la que provee el paquete cmp [disponible desde Go 1.21]) si necesitas ordenar o buscar valores máximos/mínimos. Si intentas operar con un tipo que no cumple la restricción —por ejemplo, intentar comparar dos slices usando == en un contexto que solo garantiza igualdad de tipos básicos—, el compilador lanzará un error de tipos, evitando errores catastróficos en producción.
package main
import (
"cmp" // Proporciona cmp.Ordered [disponible desde Go 1.21]
"fmt"
)
// Numeric es un constraint personalizado que define un conjunto de tipos.
// Usamos el operador tilde (~) para incluir no solo los tipos base (int, float64),
// sino también cualquier tipo definido por el usuario que tenga esos tipos subyacentes.
type Numeric interface {
~int | ~int64 | ~float64
}
// Max retorna el mayor de dos valores.
// Usamos cmp.Ordered para garantizar que el tipo T soporte operadores de comparación
// como <, <=, >, >=. Esto funciona con números y strings.
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
// Sumar permite realizar operaciones aritméticas sobre tipos que cumplen Numeric.
func Sumar[T Numeric](a, b T) T {
// El compilador sabe que T soporta el operador + porque lo limitamos a Numeric.
return a + b
}
// Identity simplemente devuelve el valor recibido, útil para demostrar any.
func Identity[T any](v T) T {
return v
}
// MyCustomInt es un tipo definido por el usuario.
type MyCustomInt int
func main() {
// Uso con tipos primitivos y cmp.Ordered
fmt.Printf("Max numérico: %v\n", Max(42, 100))
fmt.Printf("Max string: %v\n", Max("Gopher", "Zorro"))
// Uso con un tipo personalizado que satisface Numeric gracias al uso de ~
var a MyCustomInt = 10
var b MyCustomInt = 20
fmt.Printf("Suma de tipos personalizados: %v\n", Sumar(a, b))
// Uso con any
fmt.Printf("Identity: %v\n", Identity("Hola Mundo"))
}
En este ejemplo, hemos visto cómo la precisión en las restricciones define el poder de nuestras funciones. La función Max utiliza cmp.Ordered [disponible desde Go 1.21], lo que le permite trabajar tanto con int como con string, ya que ambos implementan la interfaz de ordenación. Si hubiéramos intentado usar any en Max, el compilador nos habría impedido usar a > b.
Fíjate en la interfaz Numeric: el uso de ~int es crucial. Si hubiéramos definido interface { int | float64 } (sin la tilde), el tipo MyCustomInt que definimos más abajo no habría sido válido para Sumar, porque MyCustomInt es un tipo distinto a int para el compilador, aunque su estructura interna sea la misma. La tilde le dice al compilador: “acepta cualquier tipo cuyo underlying type (tipo subyacente) sea int“.
Finalmente, Identity utiliza any, que es un alias para interface{}. Es la restricción más laxa posible, útil cuando solo necesitas mover datos de un lugar a otro sin importar qué sean o qué puedan hacer.
El error frecuente
Un error clásico ocurre cuando intentas usar un operador de comparación en un tipo genérico limitado por any o incluso por comparable de forma incorrecta.
// ESTO NO COMPILA
func IsEqual[T any](a, b T) bool {
return a == b // Error: invalid operation: a == b (type T does not implement comparable)
}
Aunque any acepta cualquier tipo, no garantiza que ese tipo sea comparable. Si intentas pasar un slice o un map a IsEqual, el compilador fallará. Para que la comparación == sea legal, el constraint debe ser explícitamente comparable.
N° 83