Generics en Go 1.18: type parameters, constraints y funciones genéricas

Go 1.18 introdujo los genéricos como la mayor ampliación del lenguaje desde su creación. Los type parameters permiten escribir funciones y tipos que funcionan con cualquier tipo que cumpla ciertas restricciones, eliminando la necesidad de duplicar código o usar interface{} con type assertions en tiempo de ejecución.

Sintaxis básica: type parameters

Los type parameters van entre corchetes después del nombre de la función o tipo. El constraint define qué métodos u operaciones puede usar el código genérico:

package main

import "fmt"

// T puede ser cualquier tipo comparable
func Contiene[T comparable](slice []T, elem T) bool {
    for _, v := range slice {
        if v == elem {
            return true
        }
    }
    return false
}

func main() {
    nums := []int{1, 2, 3, 4, 5}
    palabras := []string{"hola", "mundo", "go"}

    fmt.Println(Contiene(nums, 3))       // true
    fmt.Println(Contiene(palabras, "go")) // true

    // El compilador infiere el tipo en la llamada
    // Equivalente explícito: Contiene[int](nums, 3)
}

Constraints: any, comparable y constraints.Ordered

Un constraint es una interfaz que define qué tipos puede tomar el parámetro. Go 1.18 incluye any (alias de interface{}), comparable y el paquete golang.org/x/exp/constraints:

import "golang.org/x/exp/constraints"

// constraints.Ordered incluye todos los tipos con operadores <, >, etc.
func Min[T constraints.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

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

func main() {
    fmt.Println(Min(3, 7))         // 3
    fmt.Println(Min(3.14, 2.71))   // 2.71
    fmt.Println(Min("hola", "go")) // go (orden lexicográfico)
}

Constraints propios con interfaces de unión

Puedes definir tu propio constraint usando el operador | para listar los tipos permitidos:

// Número incluye todos los tipos numéricos integrados
type Número interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
        ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
        ~float32 | ~float64
}

func Suma[T Número](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

func main() {
    fmt.Println(Suma([]int{1, 2, 3, 4, 5}))         // 15
    fmt.Println(Suma([]float64{1.1, 2.2, 3.3}))     // 6.6000...
}

Funciones genéricas: Map, Filter y Reduce

func Map[T, U any](slice []T, fn func(T) U) []U {
    resultado := make([]U, len(slice))
    for i, v := range slice {
        resultado[i] = fn(v)
    }
    return resultado
}

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

func Reduce[T, U any](slice []T, inicial U, fn func(U, T) U) U {
    acc := inicial
    for _, v := range slice {
        acc = fn(acc, v)
    }
    return acc
}

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

    pares := Filter(nums, func(n int) bool { return n%2 == 0 })
    cuadrados := Map(pares, func(n int) int { return n * n })
    suma := Reduce(cuadrados, 0, func(acc, n int) int { return acc + n })

    fmt.Println(pares)     // [2 4 6 8 10]
    fmt.Println(cuadrados) // [4 16 36 64 100]
    fmt.Println(suma)      // 220
}

Structs genéricos: pila tipada

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 cero T
    if len(p.elementos) == 0 {
        return cero, false
    }
    n := len(p.elementos) - 1
    v := p.elementos[n]
    p.elementos = p.elementos[:n]
    return v, true
}

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

func main() {
    var pilaInt Pila[int]
    pilaInt.Push(1)
    pilaInt.Push(2)
    pilaInt.Push(3)

    for pilaInt.Len() > 0 {
        v, _ := pilaInt.Pop()
        fmt.Println(v) // 3, 2, 1
    }

    var pilaStr Pila[string]
    pilaStr.Push("hola")
    pilaStr.Push("go")
    v, _ := pilaStr.Pop()
    fmt.Println(v) // go
}

Antes de los genéricos, la única opción era usar interface{} y hacer type assertions en tiempo de ejecución, con el coste en rendimiento y seguridad que eso implica. Los genéricos resuelven esto en tiempo de compilación y el compilador genera código especializado para cada tipo concreto.

COMPARTE ESTE ARTÍCULO

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