Generics en Go: Casos de uso y patrones
Código reutilizable seguro de tipos con generics de Go
Generics en Go representan una de las características más significativas añadidas desde Go 1.0. Introducidas en Go 1.18, las generics permiten escribir código seguro de tipos, reutilizable que funciona con múltiples tipos sin sacrificar el rendimiento o la claridad del código.
Este artículo explora casos de uso prácticos, patrones comunes y mejores prácticas para aprovechar las generics en tus programas de Go. Si eres nuevo en Go o necesitas un repaso sobre los fundamentos, consulta nuestro Go Cheatsheet para construcciones y sintaxis esenciales del lenguaje.

Entendiendo las Generics en Go
Las generics en Go permiten escribir funciones y tipos parametrizados por parámetros de tipo. Esto elimina la necesidad de duplicar código cuando necesitas la misma lógica para trabajar con diferentes tipos, manteniendo la seguridad de tipo en tiempo de compilación.
Sintaxis Básica
La sintaxis para generics utiliza corchetes cuadrados para declarar parámetros de tipo:
// Función genérica
func Max[T comparable](a, b T) T {
if a > b {
return a
}
return b
}
// Uso
maxInt := Max(10, 20)
maxString := Max("apple", "banana")
Restricciones de Tipo
Las restricciones de tipo especifican qué tipos pueden usarse con tu código genérico:
any: Cualquier tipo (equivalente ainterface{})comparable: Tipos que admiten operadores==y!=- Restricciones de interfaz personalizadas: Define tus propios requisitos
// Usando una restricción personalizada
type Numeric interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64
}
func Sum[T Numeric](numbers []T) T {
var sum T
for _, n := range numbers {
sum += n
}
return sum
}
Casos de Uso Comunes
1. Estructuras de Datos Genéricas
Uno de los casos de uso más atractivos para las generics es crear estructuras de datos reutilizables:
// Pila genérica
type Stack[T any] struct {
items []T
}
func NewStack[T any]() *Stack[T] {
return &Stack[T]{items: make([]T, 0)}
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
// Uso
intStack := NewStack[int]()
intStack.Push(42)
intStack.Push(100)
stringStack := NewStack[string]()
stringStack.Push("hello")
2. Utilidades para Slices
Las generics facilitan la escritura de funciones reutilizables para manipular slices:
// Función Map
func Map[T, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
// Función Filter
func Filter[T any](slice []T, fn func(T) bool) []T {
var result []T
for _, v := range slice {
if fn(v) {
result = append(result, v)
}
}
return result
}
// Función Reduce
func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
result := initial
for _, v := range slice {
result = fn(result, v)
}
return result
}
// Uso
numbers := []int{1, 2, 3, 4, 5}
doubled := Map(numbers, func(n int) int { return n * 2 })
evens := Filter(numbers, func(n int) bool { return n%2 == 0 })
sum := Reduce(numbers, 0, func(acc, n int) int { return acc + n })
3. Utilidades Genéricas para Mapas
Trabajar con mapas se vuelve más seguro de tipo con generics:
// Obtener claves de un mapa como un slice
func Keys[K comparable, V any](m map[K]V) []K {
keys := make([]K, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return keys
}
// Obtener valores de un mapa como un slice
func Values[K comparable, V any](m map[K]V) []V {
values := make([]V, 0, len(m))
for _, v := range m {
values = append(values, v)
}
return values
}
// Obtener valor de mapa con valor predeterminado
func GetOrDefault[K comparable, V any](m map[K]V, key K, defaultValue V) V {
if v, ok := m[key]; ok {
return v
}
return defaultValue
}
4. Patrón Genérico de Opción
El patrón de opción se vuelve más elegante con generics:
type Option[T any] struct {
value *T
}
func Some[T any](value T) Option[T] {
return Option[T]{value: &value}
}
func None[T any]() Option[T] {
return Option[T]{value: nil}
}
func (o Option[T]) IsSome() bool {
return o.value != nil
}
func (o Option[T]) IsNone() bool {
return o.value == nil
}
func (o Option[T]) Unwrap() T {
if o.value == nil {
panic("intentado desempaquetar valor None")
}
return *o.value
}
func (o Option[T]) UnwrapOr(defaultValue T) T {
if o.value == nil {
return defaultValue
}
return *o.value
}
Patrones Avanzados
Composición de Restricciones
Puedes componer restricciones para crear requisitos más específicos:
type Addable interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64 | string
}
type Multiplicable interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64
}
type Numeric interface {
Addable
Multiplicable
}
func Multiply[T Multiplicable](a, b T) T {
return a * b
}
Interfaces Genéricas
También pueden ser genéricas, permitiendo abstracciones poderosas:
type Repository[T any, ID comparable] interface {
FindByID(id ID) (T, error)
Save(entity T) error
Delete(id ID) error
FindAll() ([]T, error)
}
// Implementación
type InMemoryRepository[T any, ID comparable] struct {
data map[ID]T
}
func NewInMemoryRepository[T any, ID comparable]() *InMemoryRepository[T, ID] {
return &InMemoryRepository[T, ID]{
data: make(map[ID]T),
}
}
func (r *InMemoryRepository[T, ID]) FindByID(id ID) (T, error) {
if entity, ok := r.data[id]; ok {
return entity, nil
}
var zero T
return zero, fmt.Errorf("entidad no encontrada")
}
Inferencia de Tipos
La inferencia de tipos de Go a menudo permite omitir parámetros de tipo explícitos:
// Inferencia de tipos en acción
numbers := []int{1, 2, 3, 4, 5}
// No es necesario especificar [int] - Go lo infiere
doubled := Map(numbers, func(n int) int { return n * 2 })
// Parámetros de tipo explícitos cuando sea necesario
result := Map[int, string](numbers, strconv.Itoa)
Mejores Prácticas
1. Comienza con lo Simple
No sobreutilices generics. Si una interfaz simple o un tipo concreto funcionaría, prefiera eso para una mejor legibilidad:
// Prefiere esto para casos simples
func Process(items []Processor) {
for _, item := range items {
item.Process()
}
}
// Demasiado genérico - evita
func Process[T Processor](items []T) {
for _, item := range items {
item.Process()
}
}
2. Usa Nombres de Restricciones Significativos
Nombra tus restricciones claramente para comunicar la intención:
// Bueno
type Sortable interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64 | string
}
// Menos claro
type T interface {
int | string
}
3. Documenta Restricciones Complejas
Cuando las restricciones se vuelven complejas, añade documentación:
// Numeric representa tipos que admiten operaciones aritméticas.
// Esto incluye todos los tipos de enteros y flotantes.
type Numeric interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64
}
4. Considera el Rendimiento
Las generics se compilan a tipos concretos, por lo que no hay sobrecarga en tiempo de ejecución. Sin embargo, sé consciente del tamaño del código si instancias muchas combinaciones de tipos:
// Cada combinación crea código separado
var intStack = NewStack[int]()
var stringStack = NewStack[string]()
var floatStack = NewStack[float64]()
Aplicaciones en el Mundo Real
Constructores de Consultas de Base de Datos
Las generics son especialmente útiles al construir constructores de consultas de base de datos o al trabajar con ORMs. Al trabajar con bases de datos en Go, podrías encontrar útil nuestra comparación de ORMs de Go para PostgreSQL para entender cómo las generics pueden mejorar la seguridad de tipo en operaciones de base de datos.
type QueryBuilder[T any] struct {
table string
where []string
}
func (qb *QueryBuilder[T]) Where(condition string) *QueryBuilder[T] {
qb.where = append(qb.where, condition)
return qb
}
func (qb *QueryBuilder[T]) Find() ([]T, error) {
// Implementación
return nil, nil
}
Gestión de Configuración
Cuando construyes aplicaciones de línea de comandos o sistemas de configuración, las generics pueden ayudar a crear cargadores de configuración seguros de tipo. Si estás construyendo herramientas de línea de comandos, nuestra guía sobre construir aplicaciones de CLI con Cobra & Viper demuestra cómo las generics pueden mejorar la gestión de configuración.
type ConfigLoader[T any] struct {
path string
}
func (cl *ConfigLoader[T]) Load() (T, error) {
var config T
// Cargar y deserializar configuración
return config, nil
}
Bibliotecas de Utilidad
Las generics destacan al crear bibliotecas de utilidad que funcionan con varios tipos. Por ejemplo, al generar informes o trabajar con diferentes formatos de datos, las generics pueden proporcionar seguridad de tipo. Nuestro artículo sobre generar informes PDF en Go muestra cómo las generics pueden aplicarse a utilidades de generación de informes.
Código Crítico en Rendimiento
En aplicaciones sensibles al rendimiento, como funciones serverless, las generics pueden ayudar a mantener la seguridad de tipo sin sobrecarga en tiempo de ejecución. Al considerar opciones de lenguaje para aplicaciones serverless, entender las características de rendimiento es crucial. Nuestra análisis de rendimiento de AWS Lambda en JavaScript, Python y Golang demuestra cómo el rendimiento de Go, combinado con generics, puede ser ventajoso.
Puntos Comunes de Error
1. Sobrerestricción de Tipos
Evita hacer restricciones demasiado restrictivas cuando no sea necesario:
// Demasiado restrictivo
func Process[T int | string](items []T) { }
// Mejor - más flexible
func Process[T comparable](items []T) { }
2. Ignorar la Inferencia de Tipos
Deja que Go infiera tipos cuando sea posible:
// Tipos explícitos innecesarios
result := Max[int](10, 20)
// Mejor - deja que Go infiera
result := Max(10, 20)
3. Olvidar los Valores Cero
Recuerda que los tipos genéricos tienen valores cero:
func Get[T any](slice []T, index int) (T, bool) {
if index < 0 || index >= len(slice) {
var zero T // Importante: devuelve el valor cero
return zero, false
}
return slice[index], true
}
Conclusión
Las generics en Go ofrecen una forma poderosa de escribir código seguro de tipo, reutilizable sin sacrificar el rendimiento o la legibilidad. Al entender la sintaxis, las restricciones y los patrones comunes, puedes aprovechar las generics para reducir la duplicación de código y mejorar la seguridad de tipo en tus aplicaciones de Go.
Recuerda usar las generics con juicio: no toda situación las requiere. Cuando dudes, prefiere soluciones más simples como interfaces o tipos concretos. Sin embargo, cuando necesitas trabajar con múltiples tipos mientras mantienes la seguridad de tipo, las generics son una herramienta excelente en tu kit de herramientas de Go.
A medida que Go continúa evolucionando, las generics se están convirtiendo en una característica esencial para construir aplicaciones modernas y seguras de tipo. Ya sea que estés construyendo estructuras de datos, bibliotecas de utilidad o abstracciones complejas, las generics pueden ayudarte a escribir código más limpio y mantenible.
Enlaces Útiles y Artículos Relacionados
- Tutorial de Generics en Go - Blog Oficial de Go
- Propuesta de Parámetros de Tipo
- Documentación de Generics en Go
- Go Efectivo - Generics
- Notas de la versión Go 1.18 - Generics
- Especificación del Lenguaje Go - Parámetros de Tipo
- Jugando con Generics en Go
- Cuándo usar Generics - Blog de Go
- Go Cheatsheet
- Construyendo aplicaciones de CLI en Go con Cobra & Viper
- Comparando ORMs de Go para PostgreSQL: GORM vs Ent vs Bun vs sqlc
- Generando PDF en GO - Bibliotecas y ejemplos
- Rendimiento de AWS Lambda: JavaScript vs Python vs Golang