← Volver al listado de tecnologías

Generics en Go

Por: Artiko
gogenericstype-parametersconstraints

Go 1.18 introdujo generics (polimorfismo parametrico), permitiendo escribir funciones y tipos que operan sobre multiples tipos sin sacrificar la seguridad de tipos en compilacion.

Antes de Generics

Sin generics, las opciones eran usar interface{} (perdiendo tipo) o duplicar codigo:

// Duplicacion: una funcion por tipo
func SumaInts(nums []int) int {
    var total int
    for _, n := range nums {
        total += n
    }
    return total
}

func SumaFloat64s(nums []float64) float64 {
    var total float64
    for _, n := range nums {
        total += n
    }
    return total
}

Generics resuelve esto con type parameters.

Type Parameters

Un type parameter se declara entre corchetes [T constraint] antes de los parametros regulares:

package main

import "fmt"

func Suma[T int | float64](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

func main() {
    enteros := []int{1, 2, 3, 4, 5}
    flotantes := []float64{1.1, 2.2, 3.3}

    fmt.Println(Suma(enteros))   // 15
    fmt.Println(Suma(flotantes)) // 6.6

    // Tipo explicito (opcional si Go puede inferirlo)
    fmt.Println(Suma[int](enteros)) // 15
}

Go infiere el tipo automaticamente en la mayoria de los casos.

Constraints

Las constraints definen que operaciones soporta un type parameter.

Constraints Incorporados

ConstraintDescripcion
anyCualquier tipo (alias de interface{})
comparableTipos que soportan == y !=
func Contiene[T comparable](slice []T, objetivo T) bool {
    for _, v := range slice {
        if v == objetivo {
            return true
        }
    }
    return false
}

func main() {
    fmt.Println(Contiene([]string{"a", "b", "c"}, "b")) // true
    fmt.Println(Contiene([]int{1, 2, 3}, 5))             // false
}

Constraints como Interfaces

Puedes definir constraints personalizados usando interfaces con type lists:

type Numero interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
    ~float32 | ~float64
}

func Min[T Numero](a, b T) T {
    if a < b {
        return a
    }
    return b
}

func main() {
    fmt.Println(Min(3, 7))     // 3
    fmt.Println(Min(2.5, 1.8)) // 1.8
}

El operador ~ indica tipos subyacentes: ~int incluye int y cualquier tipo definido como type MiEntero int.

Constraints con Metodos

Las constraints pueden combinar type lists y metodos:

type Stringer interface {
    ~int | ~string
    String() string
}

Esto restringe a tipos cuyo tipo subyacente sea int o string y que implementen String().

Paquete constraints (golang.org/x/exp)

El paquete experimental constraints provee constraints utiles predefinidos:

ConstraintIncluye
Signedint, int8, int16, int32, int64
Unsigneduint, uint8, uint16, uint32, uint64, uintptr
IntegerSigned + Unsigned
Floatfloat32, float64
Complexcomplex64, complex128
OrderedInteger + Float + ~string
import "golang.org/x/exp/constraints"

func Max[T constraints.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

Desde Go 1.21, el paquete cmp de la stdlib incluye cmp.Ordered:

import "cmp"

func Max[T cmp.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

Funciones Genericas

Multiples Type Parameters

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
}

func main() {
    nums := []int{1, 2, 3, 4}
    strs := Map(nums, func(n int) string {
        return fmt.Sprintf("#%d", n)
    })
    fmt.Println(strs) // [#1 #2 #3 #4]
}

Filter Generico

func Filter[T any](slice []T, pred func(T) bool) []T {
    var result []T
    for _, v := range slice {
        if pred(v) {
            result = append(result, v)
        }
    }
    return result
}

func main() {
    nums := []int{1, 2, 3, 4, 5, 6}
    pares := Filter(nums, func(n int) bool {
        return n%2 == 0
    })
    fmt.Println(pares) // [2 4 6]
}

Tipos Genericos

Los structs tambien pueden ser genericos:

package main

import "fmt"

type Pila[T any] struct {
    elementos []T
}

func (p *Pila[T]) Push(v T) {
    p.elementos = append(p.elementos, v)
}

func (p *Pila[T]) Pop() (T, bool) {
    var zero T
    if len(p.elementos) == 0 {
        return zero, false
    }
    ultimo := p.elementos[len(p.elementos)-1]
    p.elementos = p.elementos[:len(p.elementos)-1]
    return ultimo, true
}

func (p *Pila[T]) Len() int {
    return len(p.elementos)
}

func main() {
    pila := &Pila[int]{}
    pila.Push(10)
    pila.Push(20)
    pila.Push(30)

    for pila.Len() > 0 {
        v, _ := pila.Pop()
        fmt.Println(v) // 30, 20, 10
    }
}

Resultado Generico (Result Type)

type Resultado[T any] struct {
    Valor T
    Err   error
}

func NewOk[T any](v T) Resultado[T] {
    return Resultado[T]{Valor: v}
}

func NewErr[T any](err error) Resultado[T] {
    return Resultado[T]{Err: err}
}

func (r Resultado[T]) Unwrap() (T, error) {
    return r.Valor, r.Err
}

Paquetes slices y maps

Go 1.21+ incluye paquetes genericos en la stdlib:

package main

import (
    "fmt"
    "slices"
)

func main() {
    nums := []int{3, 1, 4, 1, 5, 9}

    // Ordenar
    slices.Sort(nums)
    fmt.Println(nums) // [1 1 3 4 5 9]

    // Buscar
    idx, found := slices.BinarySearch(nums, 4)
    fmt.Println(idx, found) // 3 true

    // Contiene
    fmt.Println(slices.Contains(nums, 5)) // true

    // Compactar (eliminar duplicados consecutivos)
    nums = slices.Compact(nums)
    fmt.Println(nums) // [1 3 4 5 9]

    // Reversar
    slices.Reverse(nums)
    fmt.Println(nums) // [9 5 4 3 1]
}
package main

import (
    "fmt"
    "maps"
)

func main() {
    m1 := map[string]int{"a": 1, "b": 2}
    m2 := map[string]int{"b": 3, "c": 4}

    // Copiar todos los pares de m2 a m1
    maps.Copy(m1, m2)
    fmt.Println(m1) // map[a:1 b:3 c:4]

    // Obtener claves
    for k := range maps.Keys(m1) {
        fmt.Print(k, " ")
    }

    // Comparar igualdad
    fmt.Println(maps.Equal(m1, m2)) // false
}

Cuando Usar Generics

EscenarioSolucion
Estructuras de datos (pila, cola, arbol)Generics
Funciones utilitarias (map, filter, reduce)Generics
Comportamiento polimorfico (distintas implementaciones)Interfaces
Operaciones sobre un tipo concretoFunciones regulares
Serializacion/metaprogramacionReflection

Regla practica: si escribes la misma logica para tipos distintos y solo cambia el tipo, usa generics. Si el comportamiento cambia, usa interfaces.

Limitaciones Actuales

LimitacionEstado
No hay metodos genericos en tipos no genericosNo soportado
No hay especializacion de tiposNo soportado
No hay type parameters en metodosNo soportado
No hay operador ternario genericoNo aplica en Go
Constraints no pueden ser tipos concretos sin ~Por diseno
type MiTipo struct{}

// ERROR: los metodos no pueden tener type parameters propios
// func (m MiTipo) Hacer[T any](v T) {}

// SOLUCION: usar una funcion libre
func Hacer[T any](m MiTipo, v T) {}

// O hacer el tipo generico
type MiTipo2[T any] struct{ valor T }
func (m MiTipo2[T]) Hacer() T { return m.valor }

Resumen

ConceptoSintaxis
Funcion genericafunc F[T constraint](param T) T
Tipo genericotype Nombre[T constraint] struct{}
Constraint unioninterface{ ~int | ~string }
anyCualquier tipo
comparableSoporta == y !=
cmp.OrderedSoporta <, >, <=, >=
Paquete slicesOrdenar, buscar, filtrar slices
Paquete mapsCopiar, comparar, iterar maps

← Capitulo 20: Build y Deploy | Capitulo 22: Proyecto API REST →