Generics avanzados en Go: paquetes slices, maps, cmp y patrones con constraints

Los genéricos de Go 1.18 abrieron la puerta a patrones más sofisticados que van más allá de las funciones Map y Filter básicas. Go 1.21 incorporó los paquetes slices, maps y cmp en la biblioteca estándar, consolidando las operaciones más comunes sobre colecciones sin necesidad de dependencias externas.

El paquete slices

slices cubre las operaciones que antes requerían implementación manual: ordenar, buscar, comparar y transformar slices de cualquier tipo:

package main

import (
    "cmp"
    "fmt"
    "slices"
)

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

    // Ordenar (in-place)
    slices.Sort(nums)
    fmt.Println(nums) // [1 2 3 5 8 9]

    // Búsqueda binaria
    idx, encontrado := slices.BinarySearch(nums, 5)
    fmt.Printf("5 en índice %d: %vn", idx, encontrado)

    // Comparar dos slices elemento a elemento
    a := []int{1, 2, 3}
    b := []int{1, 2, 4}
    fmt.Println(slices.Compare(a, b)) // -1 (a < b)

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

    // Eliminar duplicados (requiere slice ordenado)
    con := []int{1, 1, 2, 3, 3, 4}
    fmt.Println(slices.Compact(con)) // [1 2 3 4]

    // Revertir
    slices.Reverse(nums)
    fmt.Println(nums) // [9 8 5 3 2 1]
}

Ordenar structs con slices.SortFunc

type Producto struct {
    Nombre string
    Precio float64
    Stock  int
}

func main() {
    productos := []Producto{
        {"Ratón", 29.99, 100},
        {"Teclado", 79.99, 50},
        {"Monitor", 349.99, 15},
        {"Auriculares", 49.99, 75},
    }

    // Ordenar por precio ascendente
    slices.SortFunc(productos, func(a, b Producto) int {
        return cmp.Compare(a.Precio, b.Precio)
    })

    for _, p := range productos {
        fmt.Printf("%-15s %.2f€n", p.Nombre, p.Precio)
    }

    // Ordenar por nombre, luego por precio para empates
    slices.SortFunc(productos, func(a, b Producto) int {
        if n := cmp.Compare(a.Nombre, b.Nombre); n != 0 {
            return n
        }
        return cmp.Compare(a.Precio, b.Precio)
    })
}

El paquete maps

import "maps"

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

    // Clonar un mapa
    copia := maps.Clone(original)

    // Obtener todas las claves
    for k := range maps.Keys(original) { // Go 1.23+: iterador
        fmt.Print(k, " ")
    }

    // Copiar pares de un mapa a otro
    destino := map[string]int{"d": 4}
    maps.Copy(destino, original)
    fmt.Println(destino) // map[a:1 b:2 c:3 d:4]

    // Borrar entradas que cumplen una condición
    maps.DeleteFunc(destino, func(k string, v int) bool {
        return v < 2
    })
    fmt.Println(destino) // map[b:2 c:3 d:4]

    // Comparar dos mapas
    fmt.Println(maps.Equal(original, copia)) // true
}

El paquete cmp

cmp.Compare devuelve -1, 0 o 1 según si el primer argumento es menor, igual o mayor. cmp.Or devuelve el primer valor no-zero de la lista:

import "cmp"

// Compare: para usar en SortFunc y comparaciones
fmt.Println(cmp.Compare(3, 5))      // -1
fmt.Println(cmp.Compare("b", "a"))  // 1
fmt.Println(cmp.Compare(4.0, 4.0))  // 0

// Or: primer valor no-zero (muy útil para defaults)
nombre := cmp.Or("", "valor por defecto", "otro") // "valor por defecto"
fmt.Println(nombre)

El operador ~ en constraints

El operador ~ incluye tipos definidos sobre el tipo base, no solo el tipo exacto:

type MiInt int // tipo nuevo basado en int

type Entero interface {
    ~int | ~int32 | ~int64
}

// Con ~int, MiInt cumple el constraint
// Sin ~, MiInt no cumpliría porque no ES int, sino un tipo nuevo

func Doble[T Entero](v T) T {
    return v * 2
}

func main() {
    var x MiInt = 5
    fmt.Println(Doble(x)) // 10
}

Error clásico: type switch sobre parámetro de tipo

// INCORRECTO: no puedes hacer type switch sobre un parámetro de tipo
func procesarGenerico[T any](v T) {
    switch v.(type) { // Error de compilación
    case int:
        fmt.Println("entero")
    }
}

// CORRECTO: convertir a any primero
func procesarGenerico[T any](v T) {
    switch any(v).(type) {
    case int:
        fmt.Println("entero")
    case string:
        fmt.Println("cadena")
    default:
        fmt.Printf("tipo desconocido: %Tn", v)
    }
}

Los paquetes slices y maps de Go 1.21 ya implementan internamente este patrón, por lo que en la práctica rara vez necesitas escribir tus propias versiones genéricas de estas operaciones.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP