El compilador de Go utiliza un mecanismo de unificación de tipos para facilitar la experiencia de uso de los generics. Cuando llamas a una función genérica, el compilador intenta deducir los type arguments analizando los tipos de los valores que le pasas en los argumentos. Si el compilador puede establecer una relación unívoca entre los valores de entrada y los type constraints, omite la necesidad de que escribas explícitamente [int] o [string]. Esto es fundamental para la ergonomía, pero tiene reglas estrictas: la inferencia solo ocurre cuando el tipo de la variable está presente en los parámetros de entrada. Si un parámetro genérico solo aparece en la firma de retorno, el compilador no tiene forma de “adivinarlo” y la inferencia fallará. Además, Go no admite la inferencia parcial; o proporcionas todos los argumentos de tipo o no proporcionas ninguno.
Esta decisión de diseño busca evitar la ambigüedad. Si el compilador intentara adivinar tipos basándose en el contexto de uso o en retornos, el proceso de compilación se volvería lento y, lo que es peor, impredecible. Cuando la inferencia falla, el compilador lanza un error de tipo “cannot infer type”, obligándote a ser explícito. Si no manejas correctamente la distinción entre un tipo concreto (como int) y un tipo de interfaz (como any) durante la inferencia, puedes terminar con código que realiza más boxing/unboxing de lo necesario, afectando el rendimiento.
package example_test
import (
"testing"
)
// Identity devuelve el mismo valor que recibe.
// T se infiere del argumento 'val'.
func Identity[T any](val T) T {
return val
}
// Factory es un caso donde T NO aparece en los argumentos.
// La inferencia de T fallará aquí.
func Factory[T any]() T {
var zero T
return zero
}
// Transformer toma un valor de tipo T y una función que transforma T en R.
// El compilador debe unificar T mediante el argumento 'val'
// y R mediante el tipo de retorno de la función 'f'.
func Transformer[T any, R any](val T, f func(T) R) R {
return f(val)
}
// Mixed requiere que T y U sean resueltos.
func Mixed[T any, U any](a T, b U) (T, U) {
return a, b
}
func TestInference(t *testing.T) {
// 1. Inferencia exitosa: T se infiere como int.
val1 := Identity(42)
if val1 != 42 {
t.Error("Identity falló")
}
// 2. Error de inferencia: T solo está en el retorno.
// La siguiente línea daría error de compilación:
// val2 := Factory()
val2 := Factory[int]() // Obligatorio ser explícito.
// 3. Inferencia compuesta: T=int, R=string.
// El compilador ve que val es int y que f devuelve string.
val3 := Transformer(10, func(i int) string {
return "valor"
})
if val3 != "valor" {
t.Error("Transformer falló")
}
// 4. El problema de la inferencia parcial:
// No puedes hacer Mixed[int](1, 2.5). O pasas ambos o ninguno.
// En Go, si usas corchetes, debes llenar todos los parámetros de tipo.
_, _ = Mixed(1, 2.5) // T=int, U=float64 (inferencia total)
_, _ = Mixed[int, float64](1, 2.5) // Explícito (todo el set)
}
Análisis del ejemplo
En Identity(42), el compilador ve que 42 es un literal de tipo int. Al comparar esto con la firma func Identity[T any](val T), establece que T debe ser int.
El caso de Transformer es más complejo y es donde el motor de unificación brilla. El compilador analiza val (que es 10, un int) y determina que T = int. Luego, analiza el segundo argumento, que es una función func(int) string. Como la firma de la función dice que el retorno es string y nuestra función genérica dice que el retorno es R, el compilador deduce que R = string. La unificación es exitosa porque ambos parámetros de tipo (T y R) fueron “alimentados” por argumentos de la función.
En Mixed, observamos la restricción de la inferencia parcial. Si intentaras escribir Mixed[int](1, 2.5), el compilador de Go emitiría un error porque no puede dejar a U sin resolver. A diferencia de otros lenguajes donde podrías omitir tipos restantes, en Go la especificación exige que si abres los corchetes [], debes proveer la lista completa de argumentos de tipo.
El error frecuente
Un error clásico ocurre cuando intentas usar un tipo de interfaz, pero el compilador infiere el tipo concreto.
func Wrap[T any](v T) T {
return v
}
// Caso A: Pasamos un valor concreto
valA := Wrap(42) // T es 'int'. No es 'any'.
// Caso B: Queremos que T sea una interfaz
var v any = 42
valB := Wrap(v) // T es 'any', no 'int'.
En el Caso A, valA es de tipo int. Si intentas pasar valA a una función que solo acepta una interfaz específica de forma estricta o si esperabas que el tipo genérico fuera una interfaz para permitir mutaciones de otros tipos, la inferencia te dará el tipo concreto “más específico” encontrado, lo cual es técnicamente correcto pero suele causar errores de lógica o de tipos en el flujo de datos.
N° 84