Generics a Go: quin problema resolen realment

Generics a Go amb exemples pràctics: funcions genèriques, constraints, col·leccions i quan no fer-los servir. Sense sobre-enginyeria.

Cover for Generics a Go: quin problema resolen realment

Go va sobreviure 13 anys sense generics. Es van construir sistemes de producció enormes, CLIs que fa servir tothom i una de les infraestructures cloud més grans del planeta. Sense generics. I tot i així, funcionava. Quan van arribar a Go 1.18 (març de 2022), la comunitat es va dividir entre els que portaven una dècada demanant la funcionalitat i els que temien que Go es convertís en Java.

Quatre anys després, el balanç és clar: els generics resolen problemes reals, però el risc d’abusar-ne és igual de real. He vist codi Go amb type parameters niats tres nivells de profunditat que ningú de l’equip podia llegir. I he vist funcions genèriques de cinc línies que van eliminar centenars de línies de codi duplicat.

Aquest article va d’això: d’entendre quin problema resolen els generics, quan són l’eina correcta i, sobretot, quan no ho són. Amb codi real, sense abstraccions acadèmiques.


El problema que resolen els generics

Abans de Go 1.18, si volies escriure una funció que operés sobre tipus diferents, tenies exactament dues opcions: duplicar codi o fer servir interface{} (avui any).

Imagina que necessites una funció que retorni el valor mínim d’un slice. Sense 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 funcions idèntiques. Mateixa lògica, tipus diferent. Si trobes un bug, el corregeixes tres vegades. Si necessites int32, copies una altra vegada. Això no és un exemple teòric: la biblioteca estàndard de Go abans de 1.18 estava plena de funcions com sort.Ints, sort.Float64s, sort.Strings, cadascuna fent exactament el mateix.

L’alternativa era fer servir interface{}:

func Min(values []interface{}) interface{} {
    // Com compares dos interface{}?
    // No pots fer servir < directament.
    // Necessites type assertions o reflection.
    // Perds type safety en compilació.
    // El teu IDE no t'ajuda.
    // Runtime panic si algú passa un tipus inesperat.
}

Això no és una solució. És un pegat que intercanvia un problema (duplicació) per un de pitjor (perdre la seguretat de tipus). En llenguatges com Java o Kotlin, els generics fa dècades que resolen això. Go va decidir no incloure’ls durant 13 anys perquè l’equip no trobava un disseny que encaixés amb la filosofia del llenguatge. I honestament, crec que van fer bé en esperar.


Sintaxi bàsica: type parameters i constraints

La sintaxi dels generics a Go és deliberadament senzilla. Si véns de Java o TypeScript, et semblarà minimalista. Si només programes en Go, és nova però no difícil.

Una funció genèrica es defineix amb type parameters entre claudàtors:

func Min[T cmp.Ordered](values []T) T {
    min := values[0]
    for _, v := range values[1:] {
        if v < min {
            min = v
        }
    }
    return min
}

Desglosem:

  • [T cmp.Ordered]: declara un type parameter T amb el constraint cmp.Ordered
  • T és un placeholder: quan crides la funció, Go substitueix T pel tipus concret
  • cmp.Ordered és el constraint que diu: “T pot ser qualsevol tipus que suporti els operadors <, >, <=, >=

Per fer-la servir:

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\"})) // go

Go infereix el tipus en la majoria de casos. No cal escriure Min[int](...) explícitament, tot i que pots fer-ho si hi ha ambigüitat.

Múltiples type parameters

Pots tenir més d’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
}

// Ús
noms := Map([]int{1, 2, 3}, func(n int) string {
    return fmt.Sprintf(\"item-%d\", n)
})
// [\"item-1\", \"item-2\", \"item-3\"]

Aquí T és el tipus d’entrada i R el tipus de sortida. Cadascun té el seu propi constraint (en aquest cas, any, que és un àlies de interface{} i permet qualsevol tipus).


Constraints: quines operacions pot fer el teu tipus

Un constraint defineix què pot fer un type parameter. Sense constraints, un type parameter no pot fer res més que ser assignat i passat com a argument. No pots sumar dos T, no pots comparar-los, no pots fer res útil. El constraint és el que dona capacitats al tipus genèric.

any

El constraint més permissiu. Equivalent a interface{}. El teu tipus genèric només pot ser emmagatzemat, passat com a argument i retornat. No pots operar amb ell.

func Identity[T any](v T) T {
    return v
}

comparable

Permet fer servir == i !=. Necessari per fer servir un tipus com a clau de map o comparar valors:

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)                        // false

cmp.Ordered

Des de Go 1.21, el paquet cmp de la biblioteca estàndard defineix Ordered, que inclou tots els tipus que suporten operadors d’ordre (<, >, <=, >=). Això cobreix tots els tipus numèrics i 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)        // 7

El paquet golang.org/x/exp/constraints

Abans que cmp.Ordered arribés a la biblioteca estàndard, el paquet experimental constraints definia tipus com Integer, Float, Signed, Unsigned, Complex i Ordered. A dia d’avui (2026), per a la majoria de casos cmp.Ordered i comparable cobreixen el que necessites. El paquet experimental continua sent útil si necessites constraints més granulars com “només enters amb signe”.

Constraints personalitzats

Pots definir els teus propis constraints fent servir interfícies. Aquí és on els generics a Go es posen interessants:

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
}

L’operador ~ significa “aquest tipus o qualsevol tipus el tipus subjacent del qual sigui aquest”. Això és important perquè a Go pots definir tipus com type UserID int. Sense ~, UserID no satisfaria un constraint d’int. Amb ~int, sí que ho fa.

L’operador | és unió de tipus: “int O float32 O float64”.


Funcions genèriques: exemples pràctics

Anem al que importa. Aquestes són funcions genèriques que he fet servir o vist en codi de producció real. No són exercicis acadèmics.

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 usuaris actius
actius := Filter(usuaris, func(u Usuari) bool {
    return u.Actiu
})

// Filtrar nombres positius
positius := 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 usuari per email
user, found := Find(usuaris, func(u Usuari) bool {
    return u.Email == \"roger@oshy.tech\"
})

Fixa’t en var zero T: és el patró idiomàtic a Go per obtenir el valor zero d’un tipus genèric.

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 edats
totalEdat := Reduce(usuaris, 0, func(acc int, u Usuari) int {
    return acc + u.Edat
})

// Concatenar noms
noms := Reduce(usuaris, \"\", func(acc string, u Usuari) string {
    if acc == \"\" {
        return u.Nom
    }
    return acc + \", \" + u.Nom
})

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 usuaris per rol
perRol := GroupBy(usuaris, func(u Usuari) string {
    return u.Rol
})

Keys i Values d’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
}

Aquestes funcions existeixen ara a maps.Keys i maps.Values de la biblioteca estàndard (des de Go 1.23 amb iteradors, abans a golang.org/x/exp/maps). Però el patró és el mateix.


Tipus genèrics: col·leccions type-safe

Els generics no serveixen només per a funcions. Pots definir tipus genèrics complets. Això és especialment útil per a estructures de dades i wrappers.

Set genèric

Go no té un tipus Set natiu. Amb generics, pots construir-ne un de 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
}

Ús:

tags := NewSet(\"go\", \"backend\", \"generics\")
tags.Add(\"testing\")
tags.Contains(\"go\")      // true
tags.Contains(\"python\")  // false
tags.Len()               // 4

Abans dels generics, això es feia amb map[string]struct{} directament, sense encapsulació. O amb un wrapper sobre interface{} que perdia type safety. Ara tens un Set[string], un Set[int], un Set[UserID], tots amb seguretat de tipus en compilació.

Tipus Result

Un patró que he trobat útil per a operacions que poden fallar de maneres diferents 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
}

Això no substitueix el patró (T, error) de Go, que continua sent idiomàtic i preferible en la majoria de casos. Però és útil quan treballes amb pipelines o necessites passar resultats com a valors:

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èric

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 amb combinacions d’interfícies

Aquí és on els generics a Go mostren quelcom que altres llenguatges no tenen de forma tan neta: pots combinar mètodes i tipus en un constraint.

Constraint amb mètodes

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 amb tipus i mètodes

type OrderedStringer interface {
    cmp.Ordered
    String() string
}

Això diu: “el tipus ha de suportar operadors d’ordre I tenir un mètode String()”. A la pràctica això és poc comú perquè els tipus bàsics (int, string) no tenen mètodes. Però és útil per a tipus definits per l’usuari:

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 amb unió de tipus

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))
}

Quan fer servir generics

Els generics tenen sentit en situacions concretes. No en totes.

Funcions d’utilitat sobre col·leccions

Aquest és el cas d’ús més clar i on els generics brillen. Filter, Map, Reduce, Find, GroupBy, Contains, Unique… totes aquestes funcions operen sobre l’estructura de les dades, no sobre el seu significat. Són candidates perfectes per a generics.

La biblioteca estàndard ja n’inclou moltes amb el paquet slices (Go 1.21+):

import \"slices\"

slices.Sort(numbers)
slices.Contains(names, \"go\")
idx := slices.Index(items, target)
slices.Reverse(data)

Estructures de dades genèriques

Sets, stacks, queues, llistes enllaçades, arbres, caches LRU… qualsevol estructura de dades que sigui independent del tipus emmagatzemat es beneficia dels generics. Abans havies de triar entre type safety i reutilització. Ara pots tenir totes dues.

Reduir boilerplate repetitiu

Si tens el mateix patró repetit per a múltiples tipus i la lògica és idèntica excepte el tipus, els generics són la solució correcta. Però compte: si la lògica canvia entre tipus, no és un problema de generics, és un problema de disseny.

Wrappers i 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 amb string, int, UserID o qualsevol tipus comparable com a clau, i qualsevol tipus com a valor. Type-safe en compilació. Això abans requeria interface{} per tot arreu.


Quan NO fer servir generics

Això és tan important com saber quan fer-los servir. Potser més.

La major part de la lògica de negoci

La teva funció CreateOrder, el teu handler HTTP, el teu servei d’autenticació… res d’això necessita generics. La lògica de negoci opera sobre tipus concrets amb regles concretes. Si la teva funció ProcessPayment pren un type parameter, alguna cosa ha anat malament.

// NO facis això
func ProcessPayment[T PaymentMethod](method T, amount float64) error {
    // ...
}

// Fes això
func ProcessPayment(method PaymentMethod, amount float64) error {
    // ...
}

La segona versió fa servir una interfície normal. És més simple, més llegible i no guanyes res amb generics aquí perquè la interfície ja et dona el polimorfisme que necessites.

Quan el codi funciona bé sense ells

Si la teva funció opera sobre un sol tipus i no veus necessitat de reutilitzar-la amb altres tipus, no la facis genèrica “per si de cas”. YAGNI s’aplica amb força aquí. Go és un llenguatge que premia la simplicitat. Un func SumPrices(prices []float64) float64 és perfectament vàlid si només sumes preus.

Quan la llegibilitat pateix

// Això és llegible
func MergeSlices[T any](a, b []T) []T

// Això comença a ser qüestionable
func Transform[T any, R any, E error](items []T, fn func(T) (R, E)) ([]R, E)

// Això ja ningú ho entén 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 afegeix càrrega cognitiva. Si necessites més de dos o tres, probablement estàs sobre-enginyerant.

Per a “flexibilitat futura”

No escriguis codi genèric perquè “potser algun dia necessitaré altres tipus”. Escriu-lo quan ho necessitis. Refactoritzar una funció concreta a una de genèrica és trivial a Go. El cost de mantenir abstracció prematura no ho és.


Generics vs interfícies: triar l’abstracció correcta

Aquesta és la pregunta que més he vist en equips que comencen amb generics: “faig servir una interfície o un type parameter?”

La resposta curta: si necessites comportament diferent per tipus, fes servir interfícies. Si necessites la mateixa lògica per a tipus diferents, fes servir generics.

Interfícies: polimorfisme de comportament

type Storage interface {
    Save(ctx context.Context, key string, data []byte) error
    Load(ctx context.Context, key string) ([]byte, error)
}

// S3Storage, RedisStorage, FileStorage implementen Storage
// Cadascuna amb lògica diferent

Aquí cada implementació fa quelcom diferent. No hi ha duplicació de lògica. Les interfícies són l’eina correcta.

Generics: polimorfisme de tipus

func SortSlice[T cmp.Ordered](s []T) {
    slices.Sort(s)
}

Aquí la lògica és la mateixa sense importar si ordenes int, string o float64. Només canvia el tipus. Els generics són l’eina correcta.

La zona grisa

De vegades necessites totes dues:

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 interfície genèrica. Això és vàlid i útil quan tens múltiples repositoris (usuaris, productes, comandes) que segueixen el mateix contracte però amb tipus diferents. Cada implementació concreta pot tenir lògica diferent, però el contracte és el mateix.

Dit això, si al teu projecte només tens dos repositoris, probablement no necessites aquesta abstracció. Dues interfícies concretes UserRepository i ProductRepository són més simples i més clares. Si arribes a cinc o deu, el genèric comença a justificar-se.

Si vols aprofundir en com Go fa servir les interfícies de manera general, pots llegir-ne al Effective Go explicat.


Errors comuns amb generics

He vist aquests errors repetidament en code reviews. Intenta no caure-hi.

Fer genèric el que no ho necessita

// Innecessari. Només es fa servir amb strings.
func ParseConfig[T ~string](input T) Config {
    // ...
}

// Millor
func ParseConfig(input string) Config {
    // ...
}

Si la teva funció només es fa servir amb un tipus, el type parameter és soroll.

Constraints massa complexos

type Processable interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
    ~float32 | ~float64 | ~complex64 | ~complex128 |
    ~string
    fmt.Stringer
    json.Marshaler
}

Si el teu constraint necessita una pantalla sencera per definir-se, el teu disseny té un problema. Simplifica o fes servir una interfície normal.

No gestionar el zero value

// Bug: panic si el slice és buit
func First[T any](slice []T) T {
    return slice[0]
}

// Correcte: retorna el zero value i un bool
func First[T any](slice []T) (T, bool) {
    if len(slice) == 0 {
        var zero T
        return zero, false
    }
    return slice[0], true
}

Gestiona sempre el cas buit. El patró (T, bool) és idiomàtic a Go i funciona perfectament amb generics.

Oblidar que els type parameters no es poden fer servir en mètodes

Aquesta és una limitació real de Go (a dia d’avui, 2026): els mètodes no poden tenir type parameters propis. Només poden fer servir els type parameters del tipus receptor.

type Container[T any] struct {
    items []T
}

// VÀLID: fa servir T del tipus receptor
func (c *Container[T]) Add(item T) {
    c.items = append(c.items, item)
}

// NO COMPILA: els mètodes no poden declarar nous type parameters
// func (c *Container[T]) Map[R any](fn func(T) R) []R { ... }

La solució és fer servir una funció lliure en comptes d’un mètode:

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 és el més elegant, però és el disseny que Go va triar per mantenir la inferència de tipus simple i la compilació ràpida.


L’estat dels generics a l’ecosistema Go (2026)

Després de quatre anys amb generics, l’ecosistema ha trobat un equilibri força saludable.

La biblioteca estàndard

Els paquets slices, maps i cmp són el major beneficiari dels generics. Funcions que abans requerien paquets externs o codi duplicat ara estan a la stdlib:

import (
    \"cmp\"
    \"maps\"
    \"slices\"
)

// Ordenar un slice de qualsevol tipus comparable
slices.Sort(numbers)
slices.SortFunc(users, func(a, b User) int {
    return cmp.Compare(a.Name, b.Name)
})

// Operacions sobre maps
maps.Clone(original)
maps.DeleteFunc(m, func(k string, v int) bool {
    return v == 0
})

Els iteradors (Go 1.23) combinats amb generics han obert patrons funcionals que abans no eren possibles a Go de forma ergonòmica.

Biblioteques de la comunitat

Paquets com samber/lo (una biblioteca tipus Lodash per a Go) fan servir generics extensivament i són molt populars. samber/loFilter, Map, Reduce, Chunk, GroupBy, Uniq i desenes més. Si necessites utilitats sobre col·leccions i no vols escriure-les tu, és una opció sòlida.

Altres biblioteques destacades:

  • hashicorp/go-set: sets genèrics amb operacions de conjunts
  • emirpashas/gods: estructures de dades genèriques (arbres, cues, stacks)
  • sourcegraph/conc: concurrència type-safe amb generics

El que encara falta

Els generics a Go són deliberadament limitats. No hi ha:

  • Especialització de tipus: no pots tenir una implementació diferent d’una funció genèrica per a un tipus concret
  • Type parameters en mètodes: com he mencionat abans, els mètodes no poden declarar type parameters propis
  • Higher-kinded types: no pots abstraure sobre el constructor de tipus (no hi ha Functor ni Monad)
  • Variadic type parameters: no pots tenir un nombre variable de type parameters

Algunes d’aquestes limitacions s’estan discutint activament. Però Go continua sent Go: si alguna cosa complica el llenguatge sense un benefici clar i massiu, probablement no hi entrarà.


Exemple complet: un pipeline de processament genèric

Per tancar, un exemple que combina diversos conceptes. Un pipeline de processament batch que filtra, transforma i agrupa dades de qualsevol tipus:

package pipeline

import \"fmt\"

// Stage representa una etapa de processament
type Stage[T any] func([]T) []T

// Run executa etapes seqüencialment
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 filtratge
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
    }
}

// Ús 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
        }),
    )
}

Fixa’t que ProcessOrders és una funció concreta que fa servir el pipeline genèric. La lògica de negoci està en funcions concretes. Els generics proporcionen la infraestructura reutilitzable. Aquest és l’equilibri correcte.

Si vols veure com testejar aquest tipus de codi genèric, pots revisar com el testing a Go gestiona funcions amb type parameters. I si estàs organitzant un projecte Go des de zero, val la pena revisar les convencions d’estructura de projecte.


L’eina justa, no l’eina per defecte

Els generics a Go resolen un problema real: la duplicació de codi quan la lògica és idèntica per a tipus diferents. Les funcions d’utilitat sobre col·leccions, les estructures de dades genèriques i els wrappers type-safe són els casos d’ús clars.

Però els generics no són la resposta a tot. La major part del teu codi Go hauria de continuar sent concret, directe i sense type parameters. Go va ser dissenyat per ser simple, i els generics són una eina que s’ha de fer servir per mantenir aquesta simplicitat, no per destruir-la.

El que m”ha funcionat és escriure sempre codi concret primer i refactoritzar a genèric només quan la duplicació és real i molesta. Si necessito més de dos type parameters, sol ser un senyal que el disseny necessita un altre enfocament. I moltes vegades una interfície resol el mateix amb menys complexitat.

Els generics a Go són com el picant a la cuina: en la quantitat justa milloren tot. En excés, arruïnen el plat.

Articles relacionats

OshyTech

Enginyeria backend i de dades orientada a sistemes escalables, automatització i IA.

Navegació

Copyright 2026 OshyTech. Tots els drets reservats