Un struct genérico puede definirse utilizando parámetros de tipo, como type Pair[A, B any] struct { First A; Second B }. Esto permite crear contenedores de datos que mantienen la integridad de los tipos para sus campos sin necesidad de recurrir a interface{} o any. Sin embargo, existe una restricción fundamental en Go: aunque un método puede utilizar los parámetros de tipo ya declarados en su receptor (receiver), no puede introducir sus propios parámetros de tipo nuevos.
Esta limitación no es un capricho sintáctico, sino una decisión de diseño para mantener la viabilidad del modelo de tipos y la eficiencia del runtime. Si los métodos pudieran declarar sus propios parámetros de tipo, el conjunto de métodos de una interfaz sería potencialmente infinito, lo que haría imposible construir la itab (interface table) de forma estática. Al intentar resolver qué implementación de un método genérico llamar, el runtime tendría que gestionar una complejidad exponencial en la resolución de tipos, afectando el rendimiento del despacho de métodos.
Cuando necesitas operar sobre un tipo genérico para transformarlo en otro distinto que no tiene relación con el receptor, la solución es utilizar funciones libres (standalone functions). Si intentas forzar la lógica dentro de un método con un nuevo parámetro de tipo, el compilador fallará.
package main
import "fmt"
// Pair es un contenedor genérico para dos elementos de distintos tipos.
type Pair[A, B any] struct {
First A
Second B
}
// Swap es un método válido porque utiliza los parámetros de tipo (A y B)
// que ya pertenecen al struct receptor. No introduce nuevos tipos.
func (p Pair[A, B]) Swap() Pair[B, A] {
return Pair[B, A]{
First: p.Second,
Second: p.First,
}
}
// Transform es una función libre. Es la forma correcta de introducir
// un nuevo parámetro de tipo (C) que no está definido en la estructura original.
func Transform[A, B, C any](p Pair[A, B], f func(A, B) C) (C, C) {
res := f(p.First, p.Second)
return res, res
}
func main() {
// Inicialización de un Pair con tipos concretos (int y string).
p := Pair[int, string]{First: 42, Second: "Go"}
// Uso del método Swap: el tipo cambia de Pair[int, string] a Pair[string, int].
swapped := p.Swap()
fmt.Printf("Swap: %+v\n", swapped)
// Uso de la función libre para transformar los tipos internos a un bool.
val1, val2 := Transform(p, func(i int, s string) bool {
return i > 18 && len(s) > 0
})
fmt.Printf("Transform: %v, %v\n", val1, val2)
}
Desglose del ejemplo
En el código anterior, Pair[A, B] define dos parámetros de tipo, A y B.
El método Swap es una operación de reordenamiento. Fíjate en su firma: func (p Pair[A, B]) Swap() Pair[B, A]. Aquí, Swap no está inventando tipos nuevos; simplemente está intercambiando la posición de A y B en el tipo de retorno. El compilador puede resolver esto fácilmente porque la identidad de los tipos está ligada a la estructura Pair.
La función Transform, por el contrario, es una función independiente. Si intentáramos declarar algo como func (p Pair[A, B]) Map[C any](), el compilador lanzaría un error. Al ser una función libre, podemos pasarle A, B y C como parámetros de tipo. Esto permite que la función f (el transformador) actúe como un puente para convertir los datos de un tipo original a uno totalmente nuevo, algo que un método de la estructura no puede hacer por sí mismo.
El error frecuente
El error más común ocurre cuando intentamos implementar el patrón “Map” dentro de un método de un struct genérico.
// ESTO NO COMPILA
func (p Pair[A, B]) Map[C any](f func(A, B) C) Pair[C, C] {
// error: method cannot have type parameters
return Pair[C, C]{First: f(p.First, p.Second), Second: f(p.First, p.Second)}
}
El error method cannot have type parameters ocurre porque, aunque la lógica parezca sencilla, el compilador de Go no puede construir la tabla de métodos para una interfaz que contenga el método Map. Si una interfaz requiriera Map[C any], ¿qué implementación debería buscar el runtime cuando se asigna a una interfaz? ¿La versión para C como int o para C como string? Para evitar este caos en el despacho de métodos, Go obliga a que los nuevos tipos se gestionen mediante funciones externas.
Si la lógica de transformación es fundamental para tu dominio, considera encapsularla en un paquete de utilidades con funciones genéricas en lugar de intentar forzarla en el método del receptor.
N° 85