Generics en Go: para qué sirven y cómo usarlos
Con el lanzamiento de la versión Go 1.18, apareció en el lenguaje una de las características más esperadas: los genéricos (programación genérica). Hasta ese momento, los desarrolladores tenían que duplicar código para diferentes tipos de datos o usar la interfaz vacía interface{} con conversión de tipos posterior. Los generics resuelven este problema de forma radical. En este artículo analizaremos para qué sirven, cómo usarlos en la práctica y qué dificultades le esperan.
¿Qué son los genéricos y para qué sirven en Go?
Los genéricos (o tipos genéricos) permiten escribir funciones, estructuras y métodos que funcionan con cualquier tipo de datos, manteniendo al mismo tiempo una tipificación estricta. Esto significa que puede escribir una función que funcione tanto con números como con cadenas y tipos personalizados, sin perder información sobre el tipo en la etapa de compilación.
Antes de la aparición de los genéricos, los desarrolladores de Go usaban dos enfoques principales:
- Duplicación de código — escribir funciones separadas para
int,string,float64, etc. - Interfaz vacía — usar
interface{}con type assertion o reflexión posterior, lo que era inseguro y lento.
Los genéricos resuelven estos problemas permitiendo escribir código universal pero con seguridad de tipos. El compilador verifica los tipos en la etapa de compilación, lo que elimina errores en tiempo de ejecución.
Sintaxis de Generics: parámetros de tipo
La idea principal de los genéricos en Go son los parámetros de tipo (type parameters). Se declaran entre corchetes después del nombre de la función o tipo.
Veamos un ejemplo muy simple: una función que devuelve el mínimo de dos valores:
package main
import "fmt"
// Función genérica con parámetro de tipo Tfunc Min[T comparable](a, b T) T { if a < b { return a } return b}
func main() { fmt.Println(Min[int](10, 20)) // 10 fmt.Println(Min[string]("a", "b")) // "a" fmt.Println(Min(3.14, 2.71)) // 2.71 (el tipo se deduce automáticamente)}Aquí T es el parámetro de tipo. comparable es una restricción (constraint) que indica que el tipo T debe soportar operadores de comparación. Sin esta restricción, el compilador no permitiría usar <.
Importante: Go puede deducir automáticamente los parámetros de tipo a partir de los argumentos, por lo que en la mayoría de los casos no es necesario especificarlos explícitamente.
Restricciones (Constraints) en los genéricos de Go
Las restricciones son un elemento clave de los genéricos. Definen qué operaciones están permitidas para los parámetros de tipo. Sin restricciones, el compilador no sabe qué métodos u operadores se pueden aplicar a T.
En Go hay varias restricciones integradas:
any— sinónimo deinterface{}, permite cualquier tipo.comparable— permite tipos que soporten==y!=.constraints.Ordered— del paquetegolang.org/x/exp/constraints, permite todos los tipos ordenados (números y cadenas).
También puede crear sus propias restricciones usando interfaces:
package main
import "fmt"
// Restricción personalizada: el tipo debe tener un método String()type Stringer interface { String() string}
// Función que funciona con cualquier Stringerfunc Print[T Stringer](value T) { fmt.Println(value.String())}
type User struct { Name string}
func (u User) String() string { return "User: " + u.Name}
func main() { user := User{Name: "Alice"} Print(user) // User: Alice}Tenga en cuenta: en Go 1.18+, las interfaces se pueden usar como restricciones y pueden contener no solo métodos, sino también tipos concretos.