Generics en Go: qué problema resuelven realmente
Generics en Go con ejemplos prácticos: funciones genéricas, constraints, colecciones y cuándo no usarlos. Sin sobreingeniería.

Go sobrevivió 13 años sin generics. Se construyeron sistemas de producción enormes, CLIs que usa todo el mundo y una de las infraestructuras cloud más grandes del planeta. Sin generics. Y aun así, funcionaba. Cuando llegaron en Go 1.18 (marzo de 2022), la comunidad se dividió entre los que llevaban una década pidiendo la feature y los que temían que Go se convirtiera en Java.
Cuatro años después, el balance es claro: los generics resuelven problemas reales, pero el riesgo de abusar de ellos es igual de real. He visto código Go con type parameters anidados tres niveles profundos que nadie en el equipo podía leer. Y he visto funciones genéricas de cinco líneas que eliminaron cientos de líneas de código duplicado.
Este artículo va de eso: de entender qué problema resuelven los generics, cuándo son la herramienta correcta y, sobre todo, cuándo no lo son. Con código real, sin abstracciones académicas.
El problema que resuelven los generics
Antes de Go 1.18, si querías escribir una función que operara sobre distintos tipos, tenías exactamente dos opciones: duplicar código o usar interface{} (hoy any).
Imagina que necesitas una función que devuelva el valor mínimo de un slice. Sin generics:
func MinInt(values []int) int {
min := values[0]
for _, v := range values[1:] {
if v < min {
min = v
}
}
return min
}
func MinFloat64(values []float64) float64 {
min := values[0]
for _, v := range values[1:] {
if v < min {
min = v
}
}
return min
}
func MinString(values []string) string {
min := values[0]
for _, v := range values[1:] {
if v < min {
min = v
}
}
return min
}Tres funciones idénticas. Misma lógica, distinto tipo. Si encuentras un bug, lo corriges tres veces. Si necesitas int32, copias otra vez. Esto no es un ejemplo teórico: la librería estándar de Go antes de 1.18 estaba llena de funciones como sort.Ints, sort.Float64s, sort.Strings, cada una haciendo exactamente lo mismo.
La alternativa era usar interface{}:
func Min(values []interface{}) interface{} {
// ¿Cómo comparas dos interface{}?
// No puedes usar < directamente.
// Necesitas type assertions o reflection.
// Pierdes type safety en compilación.
// Tu IDE no te ayuda.
// Runtime panic si alguien mete un tipo inesperado.
}Esto no es una solución. Es un parche que intercambia un problema (duplicación) por otro peor (perder la seguridad de tipos). En lenguajes como Java o Kotlin, los generics llevan décadas resolviendo esto. Go decidió no incluirlos durante 13 años porque el equipo no encontraba un diseño que encajara con la filosofía del lenguaje. Y honestamente, creo que hicieron bien en esperar.
Sintaxis básica: type parameters y constraints
La sintaxis de generics en Go es deliberadamente sencilla. Si vienes de Java o TypeScript, te va a parecer minimalista. Si solo programas en Go, es nueva pero no difícil.
Una función genérica se define con type parameters entre corchetes:
func Min[T cmp.Ordered](values []T) T {
min := values[0]
for _, v := range values[1:] {
if v < min {
min = v
}
}
return min
}Desglosamos:
[T cmp.Ordered]: declara un type parameterTcon el constraintcmp.OrderedTes un placeholder: cuando llamas a la función, Go sustituyeTpor el tipo concretocmp.Orderedes el constraint que dice: “T puede ser cualquier tipo que soporte los operadores<,>,<=,>=”
Para usarla:
fmt.Println(Min([]int{3, 1, 4, 1, 5})) // 1
fmt.Println(Min([]float64{2.7, 1.4, 3.1})) // 1.4
fmt.Println(Min([]string{"go", "rust", "java"})) // goGo infiere el tipo en la mayoría de los casos. No necesitas escribir Min[int](...) explícitamente, aunque puedes hacerlo si hay ambigüedad.
Múltiples type parameters
Puedes tener más de un type parameter:
func Map[T any, R any](slice []T, fn func(T) R) []R {
result := make([]R, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
// Uso
nombres := Map([]int{1, 2, 3}, func(n int) string {
return fmt.Sprintf("item-%d", n)
})
// ["item-1", "item-2", "item-3"]Aquí T es el tipo de entrada y R el tipo de salida. Cada uno tiene su propio constraint (en este caso, any, que es un alias de interface{} y permite cualquier tipo).
Constraints: qué operaciones puede hacer tu tipo
Un constraint define qué puede hacer un type parameter. Sin constraints, un type parameter no puede hacer nada salvo ser asignado y pasado como argumento. No puedes sumar dos T, no puedes compararlos, no puedes hacer nada útil. El constraint es lo que le da capacidades al tipo genérico.
any
El constraint más permisivo. Equivale a interface{}. Tu tipo genérico solo puede ser almacenado, pasado como argumento y devuelto. No puedes operar con él.
func Identity[T any](v T) T {
return v
}comparable
Permite usar == y !=. Necesario para usar un tipo como clave de map o comparar valores:
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}
Contains([]string{"go", "rust", "python"}, "go") // true
Contains([]int{1, 2, 3}, 4) // falsecmp.Ordered
Desde Go 1.21, el paquete cmp de la librería estándar define Ordered, que incluye todos los tipos que soportan operadores de orden (<, >, <=, >=). Esto cubre todos los tipos numéricos y string.
import "cmp"
func Clamp[T cmp.Ordered](value, min, max T) T {
if value < min {
return min
}
if value > max {
return max
}
return value
}
Clamp(15, 0, 10) // 10
Clamp(-5, 0, 10) // 0
Clamp(7, 0, 10) // 7El paquete golang.org/x/exp/constraints
Antes de que cmp.Ordered llegara a la librería estándar, el paquete experimental constraints definía tipos como Integer, Float, Signed, Unsigned, Complex y Ordered. A día de hoy (2026), para la mayoría de casos cmp.Ordered y comparable cubren lo que necesitas. El paquete experimental sigue siendo útil si necesitas constraints más granulares como “solo enteros con signo”.
Constraints personalizados
Puedes definir tus propios constraints usando interfaces. Esto es donde los generics en Go se ponen interesantes:
type Number interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~float32 | ~float64
}
func Sum[T Number](values []T) T {
var total T
for _, v := range values {
total += v
}
return total
}El operador ~ significa “este tipo o cualquier tipo cuyo tipo subyacente sea este”. Esto es importante porque en Go puedes definir tipos como type UserID int. Sin ~, UserID no satisfaría un constraint de int. Con ~int, sí lo hace.
El operador | es unión de tipos: “int O float32 O float64”.
Funciones genéricas: ejemplos prácticos
Vamos a lo que importa. Estas son funciones genéricas que he usado o visto en código de producción real. No son ejercicios académicos.
Filter
func Filter[T any](slice []T, predicate func(T) bool) []T {
var result []T
for _, v := range slice {
if predicate(v) {
result = append(result, v)
}
}
return result
}
// Filtrar usuarios activos
activos := Filter(usuarios, func(u Usuario) bool {
return u.Activo
})
// Filtrar números positivos
positivos := Filter([]int{-3, -1, 0, 2, 5}, func(n int) bool {
return n > 0
})Find
func Find[T any](slice []T, predicate func(T) bool) (T, bool) {
for _, v := range slice {
if predicate(v) {
return v, true
}
}
var zero T
return zero, false
}
// Buscar un usuario por email
user, found := Find(usuarios, func(u Usuario) bool {
return u.Email == "roger@oshy.tech"
})Fíjate en var zero T: es el patrón idiomático en Go para obtener el valor zero de un tipo genérico.
Reduce
func Reduce[T any, R any](slice []T, initial R, fn func(R, T) R) R {
result := initial
for _, v := range slice {
result = fn(result, v)
}
return result
}
// Sumar edades
totalEdad := Reduce(usuarios, 0, func(acc int, u Usuario) int {
return acc + u.Edad
})
// Concatenar nombres
nombres := Reduce(usuarios, "", func(acc string, u Usuario) string {
if acc == "" {
return u.Nombre
}
return acc + ", " + u.Nombre
})GroupBy
func GroupBy[T any, K comparable](slice []T, keyFn func(T) K) map[K][]T {
result := make(map[K][]T)
for _, v := range slice {
key := keyFn(v)
result[key] = append(result[key], v)
}
return result
}
// Agrupar usuarios por rol
porRol := GroupBy(usuarios, func(u Usuario) string {
return u.Rol
})Keys y Values de un map
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
}
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
}Estas funciones existen ahora en maps.Keys y maps.Values de la librería estándar (desde Go 1.23 con iteradores, antes en golang.org/x/exp/maps). Pero el patrón es el mismo.
Tipos genéricos: colecciones type-safe
Los generics no solo sirven para funciones. Puedes definir tipos genéricos completos. Esto es especialmente útil para estructuras de datos y wrappers.
Set genérico
Go no tiene un tipo Set nativo. Con generics, puedes construir uno type-safe:
type Set[T comparable] struct {
items map[T]struct{}
}
func NewSet[T comparable](values ...T) *Set[T] {
s := &Set[T]{items: make(map[T]struct{})}
for _, v := range values {
s.Add(v)
}
return s
}
func (s *Set[T]) Add(value T) {
s.items[value] = struct{}{}
}
func (s *Set[T]) Contains(value T) bool {
_, ok := s.items[value]
return ok
}
func (s *Set[T]) Remove(value T) {
delete(s.items, value)
}
func (s *Set[T]) Len() int {
return len(s.items)
}
func (s *Set[T]) Values() []T {
values := make([]T, 0, len(s.items))
for k := range s.items {
values = append(values, k)
}
return values
}Uso:
tags := NewSet("go", "backend", "generics")
tags.Add("testing")
tags.Contains("go") // true
tags.Contains("python") // false
tags.Len() // 4Antes de generics, esto se hacía con map[string]struct{} directamente, sin encapsulación. O con un wrapper sobre interface{} que perdía type safety. Ahora tienes un Set[string], un Set[int], un Set[UserID], todos con seguridad de tipos en compilación.
Result type
Un patrón que he encontrado útil para operaciones que pueden fallar de formas distintas a un simple error:
type Result[T any] struct {
Value T
Err error
}
func OK[T any](value T) Result[T] {
return Result[T]{Value: value}
}
func Fail[T any](err error) Result[T] {
return Result[T]{Err: err}
}
func (r Result[T]) IsOK() bool {
return r.Err == nil
}
func (r Result[T]) Unwrap() (T, error) {
return r.Value, r.Err
}Esto no reemplaza el patrón (T, error) de Go, que sigue siendo idiomático y preferible en la mayoría de casos. Pero es útil cuando trabajas con pipelines o necesitas pasar resultados como valores:
func ProcessBatch[T any](items []T, process func(T) Result[T]) []Result[T] {
results := make([]Result[T], len(items))
for i, item := range items {
results[i] = process(item)
}
return results
}Stack genérico
type Stack[T any] struct {
items []T
}
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
}
func (s *Stack[T]) Peek() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
return s.items[len(s.items)-1], true
}
func (s *Stack[T]) Len() int {
return len(s.items)
}Constraints con combinaciones de interfaces
Aquí es donde los generics en Go muestran algo que otros lenguajes no tienen de forma tan limpia: puedes combinar métodos y tipos en un constraint.
Constraint con métodos
type Stringer interface {
String() string
}
func JoinStrings[T Stringer](items []T, sep string) string {
parts := make([]string, len(items))
for i, item := range items {
parts[i] = item.String()
}
return strings.Join(parts, sep)
}Constraint con tipos y métodos
type OrderedStringer interface {
cmp.Ordered
String() string
}Esto dice: “el tipo debe soportar operadores de orden Y tener un método String()”. En la práctica esto es poco común porque los tipos básicos (int, string) no tienen métodos. Pero es útil para tipos definidos por el usuario:
type Priority int
func (p Priority) String() string {
switch p {
case 1:
return "low"
case 2:
return "medium"
case 3:
return "high"
default:
return "unknown"
}
}Constraint con unión de tipos
type Numeric interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64
}
func Avg[T Numeric](values []T) float64 {
if len(values) == 0 {
return 0
}
var sum T
for _, v := range values {
sum += v
}
return float64(sum) / float64(len(values))
}Cuándo usar generics
Los generics tienen sentido en situaciones concretas. No en todas.
Funciones de utilidad sobre colecciones
Este es el caso de uso más claro y donde los generics brillan. Filter, Map, Reduce, Find, GroupBy, Contains, Unique… todas estas funciones operan sobre la estructura de los datos, no sobre su significado. Son candidatas perfectas para generics.
La librería estándar ya incluye muchas con el paquete slices (Go 1.21+):
import "slices"
slices.Sort(numbers)
slices.Contains(names, "go")
idx := slices.Index(items, target)
slices.Reverse(data)Estructuras de datos genéricas
Sets, stacks, queues, linked lists, trees, caches LRU… cualquier estructura de datos que sea independiente del tipo almacenado se beneficia de generics. Antes tenías que elegir entre type safety y reutilización. Ahora puedes tener ambas.
Reducir boilerplate repetitivo
Si tienes el mismo patrón repetido para múltiples tipos y la lógica es idéntica salvo el tipo, los generics son la solución correcta. Pero cuidado: si la lógica cambia entre tipos, no es un problema de generics, es un problema de diseño.
Wrappers y adapters
type Cache[K comparable, V any] struct {
data map[K]V
mu sync.RWMutex
maxSize int
}
func NewCache[K comparable, V any](maxSize int) *Cache[K, V] {
return &Cache[K, V]{
data: make(map[K]V),
maxSize: maxSize,
}
}
func (c *Cache[K, V]) Get(key K) (V, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.data[key]
return v, ok
}
func (c *Cache[K, V]) Set(key K, value V) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}Un cache que funciona con string, int, UserID o cualquier tipo comparable como clave, y cualquier tipo como valor. Type-safe en compilación. Esto antes requería interface{} por todas partes.
Cuándo NO usar generics
Esto es tan importante como saber cuándo usarlos. Puede que más.
La mayor parte de la lógica de negocio
Tu función CreateOrder, tu handler HTTP, tu servicio de autenticación… nada de esto necesita generics. La lógica de negocio opera sobre tipos concretos con reglas concretas. Si tu función ProcessPayment toma un type parameter, algo ha ido mal.
// NO hagas esto
func ProcessPayment[T PaymentMethod](method T, amount float64) error {
// ...
}
// Haz esto
func ProcessPayment(method PaymentMethod, amount float64) error {
// ...
}La segunda versión usa una interfaz normal. Es más simple, más legible y no ganas nada con generics aquí porque la interfaz ya te da el polimorfismo que necesitas.
Cuando el código funciona bien sin ellos
Si tu función opera sobre un solo tipo y no ves necesidad de reutilizarla con otros tipos, no la hagas genérica “por si acaso”. YAGNI aplica con fuerza aquí. Go es un lenguaje que premia la simplicidad. Un func SumPrices(prices []float64) float64 es perfectamente válido si solo sumas precios.
Cuando la legibilidad sufre
// Esto es legible
func MergeSlices[T any](a, b []T) []T
// Esto empieza a ser cuestionable
func Transform[T any, R any, E error](items []T, fn func(T) (R, E)) ([]R, E)
// Esto ya nadie lo entiende a primera vista
func Pipeline[I any, M any, O any](
input []I,
stage1 func(I) (M, error),
stage2 func(M) (O, error),
) ([]O, error)Cada type parameter añade carga cognitiva. Si necesitas más de dos o tres, probablemente estás sobreingenierando.
Para “flexibilidad futura”
No escribas código genérico porque “quizás algún día necesite otros tipos”. Escríbelo cuando lo necesites. Refactorizar una función concreta a una genérica es trivial en Go. El coste de mantener abstracción prematura no lo es.
Generics vs interfaces: elegir la abstracción correcta
Esta es la pregunta que más he visto en equipos que empiezan con generics: “¿uso una interfaz o un type parameter?”
La respuesta corta: si necesitas comportamiento diferente por tipo, usa interfaces. Si necesitas la misma lógica para tipos diferentes, usa generics.
Interfaces: polimorfismo de comportamiento
type Storage interface {
Save(ctx context.Context, key string, data []byte) error
Load(ctx context.Context, key string) ([]byte, error)
}
// S3Storage, RedisStorage, FileStorage implementan Storage
// Cada una con lógica diferenteAquí cada implementación hace algo distinto. No hay duplicación de lógica. Las interfaces son la herramienta correcta.
Generics: polimorfismo de tipo
func SortSlice[T cmp.Ordered](s []T) {
slices.Sort(s)
}Aquí la lógica es la misma sin importar si ordenas int, string o float64. Solo cambia el tipo. Generics son la herramienta correcta.
La zona gris
A veces necesitas ambos:
type Repository[T any] interface {
FindByID(ctx context.Context, id string) (T, error)
Save(ctx context.Context, entity T) error
Delete(ctx context.Context, id string) error
}Una interfaz genérica. Esto es válido y útil cuando tienes múltiples repositorios (usuarios, productos, pedidos) que siguen el mismo contrato pero con tipos distintos. Cada implementación concreta puede tener lógica diferente, pero el contrato es el mismo.
Dicho esto, si en tu proyecto solo tienes dos repositorios, probablemente no necesitas esta abstracción. Dos interfaces concretas UserRepository y ProductRepository son más simples y más claras. Si llegas a cinco o diez, el genérico empieza a justificarse.
Si quieres profundizar en cómo Go usa las interfaces de forma general, puedes leer sobre ello en Effective Go explicado.
Errores comunes con generics
He visto estos errores repetidamente en code reviews. Intenta no caer en ellos.
Hacer genérico lo que no lo necesita
// Innecesario. Solo se usa con strings.
func ParseConfig[T ~string](input T) Config {
// ...
}
// Mejor
func ParseConfig(input string) Config {
// ...
}Si tu función solo se usa con un tipo, el type parameter es ruido.
Constraints demasiado complejos
type Processable interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64 | ~complex64 | ~complex128 |
~string
fmt.Stringer
json.Marshaler
}Si tu constraint necesita una pantalla entera para definirse, tu diseño tiene un problema. Simplifica o usa una interfaz normal.
No manejar el zero value
// Bug: panic si el slice está vacío
func First[T any](slice []T) T {
return slice[0]
}
// Correcto: devuelve el zero value y un bool
func First[T any](slice []T) (T, bool) {
if len(slice) == 0 {
var zero T
return zero, false
}
return slice[0], true
}Siempre maneja el caso vacío. El patrón (T, bool) es idiomático en Go y funciona perfecto con generics.
Olvidar que los type parameters no se pueden usar en methods
Esto es una limitación real de Go (a día de hoy, 2026): los métodos no pueden tener type parameters propios. Solo pueden usar los type parameters del tipo receptor.
type Container[T any] struct {
items []T
}
// VÁLIDO: usa T del tipo receptor
func (c *Container[T]) Add(item T) {
c.items = append(c.items, item)
}
// NO COMPILA: los métodos no pueden declarar nuevos type parameters
// func (c *Container[T]) Map[R any](fn func(T) R) []R { ... }La solución es usar una función libre en vez de un método:
func MapContainer[T any, R any](c *Container[T], fn func(T) R) []R {
result := make([]R, len(c.items))
for i, item := range c.items {
result[i] = fn(item)
}
return result
}No es lo más elegante, pero es el diseño que Go eligió para mantener la inferencia de tipos simple y la compilación rápida.
El estado de los generics en el ecosistema Go (2026)
Después de cuatro años con generics, el ecosistema ha encontrado un equilibrio bastante sano.
La librería estándar
Los paquetes slices, maps y cmp son el mayor beneficiario de los generics. Funciones que antes requerían paquetes externos o código duplicado ahora están en la stdlib:
import (
"cmp"
"maps"
"slices"
)
// Ordenar un slice de cualquier tipo comparable
slices.Sort(numbers)
slices.SortFunc(users, func(a, b User) int {
return cmp.Compare(a.Name, b.Name)
})
// Operaciones sobre maps
maps.Clone(original)
maps.DeleteFunc(m, func(k string, v int) bool {
return v == 0
})Los iteradores (Go 1.23) combinados con generics han abierto patrones funcionales que antes no eran posibles en Go de forma ergonómica.
Librerías de la comunidad
Paquetes como samber/lo (una librería tipo Lodash para Go) usan generics extensivamente y son muy populares. samber/lo tiene Filter, Map, Reduce, Chunk, GroupBy, Uniq y decenas más. Si necesitas utilidades sobre colecciones y no quieres escribirlas tú, es una opción sólida.
Otras librerías notables:
hashicorp/go-set: sets genéricos con operaciones de conjuntosemirpashas/gods: estructuras de datos genéricas (trees, queues, stacks)sourcegraph/conc: concurrencia type-safe con generics
Lo que todavía falta
Los generics en Go son deliberadamente limitados. No hay:
- Especialización de tipos: no puedes tener una implementación diferente de una función genérica para un tipo concreto
- Type parameters en métodos: como mencioné antes, los métodos no pueden declarar type parameters propios
- Higher-kinded types: no puedes abstraer sobre el constructor de tipos (no hay
FunctorniMonad) - Variadic type parameters: no puedes tener un número variable de type parameters
Algunas de estas limitaciones se están discutiendo activamente. Pero Go sigue siendo Go: si algo complica el lenguaje sin un beneficio claro y masivo, probablemente no entrará.
Ejemplo completo: un pipeline de procesamiento genérico
Para cerrar, un ejemplo que combina varios conceptos. Un pipeline de procesamiento batch que filtra, transforma y agrupa datos de cualquier tipo:
package pipeline
import "fmt"
// Stage representa una etapa de procesamiento
type Stage[T any] func([]T) []T
// Pipeline ejecuta etapas secuencialmente
func Run[T any](data []T, stages ...Stage[T]) []T {
result := data
for _, stage := range stages {
result = stage(result)
}
return result
}
// FilterStage crea una etapa de filtrado
func FilterStage[T any](predicate func(T) bool) Stage[T] {
return func(items []T) []T {
var result []T
for _, item := range items {
if predicate(item) {
result = append(result, item)
}
}
return result
}
}
// Uso real
type Order struct {
ID string
Amount float64
Status string
}
func ProcessOrders(orders []Order) []Order {
return Run(orders,
FilterStage(func(o Order) bool {
return o.Status == "confirmed"
}),
FilterStage(func(o Order) bool {
return o.Amount > 100
}),
)
}Fíjate en que ProcessOrders es una función concreta que usa el pipeline genérico. La lógica de negocio está en funciones concretas. Los generics proporcionan la infraestructura reutilizable. Ese es el equilibrio correcto.
Si quieres ver cómo testear este tipo de código genérico, puedes revisar cómo el testing en Go maneja funciones con type parameters. Y si estás organizando un proyecto Go desde cero, vale la pena revisar las convenciones de estructura de proyecto.
La herramienta justa, no la herramienta por defecto
Los generics en Go resuelven un problema real: la duplicación de código cuando la lógica es idéntica para distintos tipos. Las funciones de utilidad sobre colecciones, las estructuras de datos genéricas y los wrappers type-safe son los casos de uso claros.
Pero los generics no son la respuesta a todo. La mayor parte de tu código Go debería seguir siendo concreto, directo y sin type parameters. Go fue diseñado para ser simple, y los generics son una herramienta que debe usarse para mantener esa simplicidad, no para destruirla.
Lo que me ha funcionado es escribir siempre código concreto primero y refactorizar a genérico solo cuando la duplicación es real y molesta. Si necesito más de dos type parameters, suele ser una señal de que el diseño necesita otro enfoque. Y muchas veces una interfaz resuelve lo mismo con menos complejidad.
Los generics en Go son como el picante en la cocina: en la cantidad justa mejoran todo. En exceso, arruinan el plato.


