Llevo escribiendo sobre Go en este blog desde 2015. En aquel artículo de “Qué me gusta de Go” hablaba de la simplicidad, de la compilación rápida, de lo bien que sienta un lenguaje que no te obliga a memorizar veinte formas de hacer lo mismo. Y luego llegó Go 1.18 con los genéricos, en 2022, y con ellos una asimetría que ha incomodado a mucha gente durante años: podías escribir tipos genéricos y funciones genéricas, pero no métodos genéricos.
El artículo de Mark Freeman en el blog oficial de Go, publicado el 26 de agosto, cierra ese capítulo. Go 1.27 añade parámetros de tipo a los métodos de tipos concretos.
La asimetría que molestaba
Go 1.18 resolvió el problema clásico: antes tenías que escribir una lista enlazada para enteros y otra para cadenas.
// antes de Go 1.18
type ListOfInts struct {
elem int
next *ListOfInts
}
type ListOfStrings struct {
elem string
next *ListOfStrings
}
// después de Go 1.18
type List[E any] struct {
elem E
next *List[E]
}
Lo mismo con las funciones: SortInts y SortStrings se convirtieron en un único Sort[E cmp.Ordered]. Perfecto.
Pero los métodos se quedaron fuera. La propuesta original de genéricos razonaba que, como los métodos genéricos en interfaces son difíciles de implementar de forma eficiente, tampoco tenía sentido añadir parámetros de tipo a los métodos concretos. En esa visión, los métodos son fundamentalmente el mecanismo para implementar interfaces, así que si una mitad no puede existir, la otra tampoco.
Dónde dolía en la práctica
El ejemplo del artículo es muy claro. Tienes tu List[E] y quieres transformarla en una lista de otro tipo. Puedes escribir un método específico:
func (List[E]) ToString(f func(E) string) List[string] { /* ... */ }
Y funciona bien para casos concretos:
fmt.Println(NewList(1, 2, 3).ToString(strconv.Itoa)) // [1 2 3]
El problema es que parametrizar List te permite generalizar el tipo de origen, pero no el de destino, porque el destino depende de la transformación que apliques. Si solo tienes un tipo de destino, vale. Si tienes muchos, necesitas una función genérica:
// después de Go 1.18
func MapList[E, R any](l List[E], f func(E) R) List[R] { /* ... */ }
Y aquí aparecen dos inconvenientes. El primero es de organización: MapList vive en el ámbito del paquete, y si tienes varios tipos de datos que soportan operaciones de mapeo, el paquete se llena de funciones sueltas. El segundo es de legibilidad: las llamadas encadenadas hay que escribirlas de dentro hacia afuera.
fmt.Println(MapList(MapList(NewList(0, 2, 4), add(2)), divideBy(2))) // [1 2 3]
Cualquiera que haya escrito una cadena de tres o cuatro transformaciones sabe lo que es leer eso.
Lo que trae Go 1.27
// después de Go 1.27
func (List[E]) Map[R any](f func(E) R) List[R] { /* ... */ }
Compacto, expresivo y con ámbito local al tipo. Y las llamadas encadenadas se leen de izquierda a derecha, como uno esperaría:
fmt.Println(NewList(0, 2, 4).Map(add(2)).Map(divideBy(2))) // [1 2 3]
Si por lo que sea prefieres la forma “de dentro hacia afuera”, las expresiones de método siguen funcionando con métodos genéricos:
f := List[int].Map[int]
fmt.Println(f(f(NewList(0, 2, 4), add(2)), divideBy(2))) // [1 2 3]
Como el resto de genéricos en Go, los métodos genéricos deben instanciarse —explícita o implícitamente— antes de usarse.
El cambio de mentalidad
Lo que me parece más interesante del artículo no es la sintaxis, sino el razonamiento. El equipo de Go ha desacoplado dos conceptos que antes iban juntos: los métodos como mecanismo para implementar interfaces, y los métodos como herramienta de organización del código.
Un método concreto genérico no puede ayudar a implementar una interfaz, eso sigue siendo cierto. Pero sí puede ser útil para organizar código. Y esa segunda utilidad es suficiente por sí misma.
El artículo lo ilustra con un caso sutil:
type I interface {
M()
}
type T struct{} // un struct, no una interfaz
func (T) M[P any]() { /* ... */ } // un método *concreto* genérico
Cualquier instanciación de T.M produce una firma idéntica a la de I.M —ambas son func M()—. Pero eso no significa que T implemente I. La implementación de interfaces es una propiedad del tipo, no de un método concreto: T no declara T.M[int], declara su contrapartida genérica sin instanciar T.M.
Por qué no hay métodos genéricos en interfaces
Esta es la parte que explica ocho años de espera, y merece la pena entenderla porque dice mucho sobre cómo compila Go.
Un valor de interfaz es una caja que puede contener un valor de cualquier tipo que la implemente. Cuando llamas a i.M(), en un programa sencillo es evidente que la llamada va a parar a T.M. Pero en programas reales esa relación cruza fronteras de paquete y es imposible de deducir en el caso general.
// -- paquete main --
import "p"
func main() {
p.F(T{})
}
type T struct{}
func (T) M() { /* ... */ }
// -- paquete p --
func F(i I) {
i.M()
}
type I interface {
M()
}
Los dos paquetes se compilan por separado, así que main no sabe qué hace p.F con T{}. La solución actual del compilador es sencilla: genera código para todos los métodos no genéricos de un tipo en su declaración o instanciación. Así, haga lo que haga p.F, el código existirá en tiempo de ejecución.
Ahora imagina que I.M fuera genérico:
// -- paquete p --
func F(i I) {
i.M[int]()
}
type I interface {
M[P any]()
}
Para cubrir cualquier uso posible de T{} dentro de p.F, el compilador tendría que instanciar T.M con todos los argumentos de tipo posibles. Eso es inviable con el enfoque de Go, que genera código específico para cada instanciación.
Existe una alternativa: si los argumentos de tipo se “encajonaran” —pasándolos como valores de sus interfaces de restricción—, todas las instanciaciones compartirían el mismo código. Evitarías la explosión de instanciaciones, pero a cambio pagarías el coste de llamadas indirectas, incluso en las llamadas directas a métodos ya instanciados. Es exactamente el tipo de trade-off que Go lleva años negándose a aceptar, y me parece coherente.
Mi lectura
Go sigue siendo el lenguaje que me gustó en 2015 por razones que no han cambiado: prefiere resolver el 90% del problema con una solución que no tiene coste oculto, antes que resolver el 100% pagando en rendimiento o complejidad. Los métodos genéricos son un buen ejemplo. No tenemos la versión completa de la característica, tenemos la mitad que se puede implementar bien.
Y esa mitad es la que más se usa en el día a día. Escribir Map, Filter o Reduce como métodos de tus propios tipos, con encadenamiento legible, cubre la inmensa mayoría de los casos en los que uno echaba de menos los métodos genéricos. Los métodos genéricos en interfaces son un problema de diseño de compiladores que quizá se resuelva algún día, o quizá no.
Ocho años entre la propuesta de genéricos y esta pieza que faltaba. En un ecosistema donde cada seis meses aparece un lenguaje que promete resolverlo todo, esa cadencia lenta y deliberada sigue siendo, para mí, una de las mejores cosas de Go.









I’ve always liked to see and try different technologies, and within these, of course, programming languages.
Comentarios