Genéricos en Go

Los genéricos son una característica importante introducida en la versión 1.18 de Go, que permite a los desarrolladores escribir código más flexible y reutilizable.

Los genéricos se implementan principalmente mediante los siguientes dos conceptos centrales:

  • Parámetros de tipo (Type Parameters):Permiten usar uno o más tipos como parámetros en funciones o definiciones de tipos.
  • Restricciones de tipo (Type Constraints):Especifican las condiciones que deben cumplir los parámetros de tipo, garantizando que se puedan operar de forma segura dentro de la función.
Concepto Función Ejemplo
Parámetro de tipo Se declara después del nombre de la función o tipo, representando un tipo por determinar. [T any]
Restricción de tipo Define las condiciones que deben cumplir los parámetros de tipo (como operadores o métodos soportados). int,float64,comparable,constraints.Ordered,any
any Restringe el parámetro de tipo acualquiertipo. [T any]
comparable Restringe el parámetro de tipo acomparabletipos. [K comparable]

Los genéricos nos permiten escribir código que no depende de un tipo de datos específico.

Antes de la introducción de los genéricos, si queríamos manejar diferentes tipos de datos, normalmente necesitábamos escribir funciones duplicadas para cada tipo.

Limitaciones del enfoque tradicional:

Ejemplo

// Función para manejar el tipo int
func MaxInt(a, b int) int {
    if a > b {
        return a
    }
    return b
}

// Función para manejar el tipo float64
func MaxFloat(a, b float64) float64 {
    if a > b {
        return a
    }
    return b
}

Solución usando genéricos:

Ejemplo

// Una función para manejar múltiples tipos
func Max[T comparable](a, b T) T {
    if a > b {
        return a
    }
    return b
}

Explicación detallada de la sintaxis de genéricos

Declaración de parámetros de tipo

Las funciones y tipos genéricos se declaran mediante una lista de parámetros de tipo, con la sintaxis:[Restricción de parámetro de tipo]。

Ejemplo

// Estructura básica de sintaxis
funcNombre de la función[T restricción](Parámetro T)Tipo de retorno{
    // Cuerpo de la función
}

typeNombre del tipo[T restricción] struct {
    // Campos de la estructura
}

Convenciones de nombres para parámetros de tipo

  • Normalmente se utilizan letras mayúsculas:T、K、V、Eesperar
  • T: representa Type (tipo)
  • K: representa Key (clave)
  • V: representa Value (valor)
  • E: representa Element (elemento)

Restricciones (Constraints)

Las restricciones definen las condiciones que deben cumplir los parámetros de tipo; son el concepto central de los genéricos.

Restricciones integradas

1. anyLa restricción

anyes la interfaz vacíainterface{}es un alias, lo que significa que cualquier tipo es válido.

Ejemplo

func PrintAny[T any](value T) {
    fmt.Printf("Value: %v, Type: %T\n", value, value)
}

// Ejemplo de uso
PrintAny(42)        // Value: 42, Type: int
PrintAny("hello")   // Value: hello, Type: string
PrintAny(3.14)      // Value: 3.14, Type: float64

2. comparableLa restricción

comparableindica que el tipo soporta==y!=el operador.

Ejemplo

func FindIndex[T comparable](slice []T, target T) int {
    for i, v := range slice {
        if v == target {
            return i
        }
    }
    return -1
}

// Ejemplo de uso
numbers := []int{1, 2, 3, 4, 5}
fmt.Println(FindIndex(numbers, 3))  // Salida: 2

names := []string{"Alice", "Bob", "Charlie"}
fmt.Println(FindIndex(names, "Bob"))  // Salida: 1

3. Restricciones de unión (Union Constraints)

Usa|el operador | para combinar múltiples tipos.

Ejemplo

// Restricción de tipos numéricos
type Number interface {
    int | int8 | int16 | int32 | int64 |
    uint | uint8 | uint16 | uint32 | uint64 |
    float32 | float64
}

func Add[T Number](a, b T) T {
    return a + b
}

// Ejemplo de uso
fmt.Println(Add(10, 20))        // Salida: 30
fmt.Println(Add(3.14, 2.71))    // Salida: 5.85

Restricciones personalizadas

1. Restricciones de métodos

Define restricciones que requieren métodos específicos.

Ejemplo

// Definir la restricción Stringer
type Stringer interface {
    String() string
}

func PrintString[T Stringer](value T) {
    fmt.Println(value.String())
}

// Implementar un tipo personalizado
type Person struct {
    Name string
    Age  int
}

func (p Person) String() string {
    return fmt.Sprintf("%s (%d years old)", p.Name, p.Age)
}

// Ejemplo de uso
person := Person{Name: "Alice", Age: 25}
PrintString(person)  // Salida: Alice (25 years old)

2. Restricciones complejas

Combina requisitos de tipos y métodos.

Ejemplo

// Requiere que el tipo sea numérico y que implemente el método String()
type NumericStringer interface {
    Number
    String() string
}

Práctica de funciones genéricas

1. Funciones de utilidad generales

Ejemplo

// Intercambiar dos valores
func Swap[T any](a, b T) (T, T) {
    return b, a
}

// Comprobar si un slice contiene un elemento
func Contains[T comparable](slice []T, target T) bool {
    for _, item := range slice {
        if item == target {
            return true
        }
    }
    return false
}

// Función para eliminar duplicados
func Unique[T comparable](slice []T) []T {
    seen := make(map[T]bool)
    result := []T{}
   
    for _, item := range slice {
        if !seen[item] {
            seen[item] = true
            result = append(result, item)
        }
    }
    return result
}

// Ejemplo de uso
func main() {
    // Ejemplo de Swap
    a, b := 10, 20
    a, b = Swap(a, b)
    fmt.Printf("a=%d, b=%d\n", a, b)  // Salida: a=20, b=10
   
    // Ejemplo de Contains
    numbers := []int{1, 2, 3, 4, 5}
    fmt.Println(Contains(numbers, 3))  // Salida: true
   
    // Ejemplo de Unique
    duplicates := []int{1, 2, 2, 3, 4, 4, 5}
    unique := Unique(duplicates)
    fmt.Println(unique)  // Salida: [1 2 3 4 5]
}

2. Funciones de operaciones matemáticas

Ejemplo

// Obtener el valor máximo de un slice
func Max[T Number](slice []T) T {
    if len(slice) == 0 {
        var zero T
        return zero
    }
   
    max := slice[0]
    for _, value := range slice[1:] {
        if value > max {
            max = value
        }
    }
    return max
}

// Obtener el valor mínimo de un slice
func Min[T Number](slice []T) T {
    if len(slice) == 0 {
        var zero T
        return zero
    }
   
    min := slice[0]
    for _, value := range slice[1:] {
        if value < min {
            min = value
        }
    }
    return min
}

// Obtener el promedio de un slice
func Average[T Number](slice []T) float64 {
    if len(slice) == 0 {
        return 0
    }
   
    var sum T
    for _, value := range slice {
        sum += value
    }
    return float64(sum) / float64(len(slice))
}

// Ejemplo de uso
func main() {
    ints := []int{1, 5, 3, 9, 2}
    floats := []float64{1.1, 5.5, 3.3, 9.9, 2.2}
   
    fmt.Printf("Max int: %d\n", Max(ints))           // Salida: 9
    fmt.Printf("Min float: %.1f\n", Min(floats))     // Salida: 1.1
    fmt.Printf("Average: %.2f\n", Average(floats))   // Salida: 4.40
}

Tipos genéricos

1. Estructuras genéricas

Ejemplo

// Implementación de una pila genérica
type Stack[T any] struct {
    elements []T
}

// Apilar (push)
func (s *Stack[T]) Push(value T) {
    s.elements = append(s.elements, value)
}

// Desapilar (pop)
func (s *Stack[T]) Pop() (T, bool) {
    if len(s.elements) == 0 {
        var zero T
        return zero, false
    }
   
    lastIndex := len(s.elements) - 1
    value := s.elements[lastIndex]
    s.elements = s.elements[:lastIndex]
    return value, true
}

// Ver el elemento superior de la pila
func (s *Stack[T]) Peek() (T, bool) {
    if len(s.elements) == 0 {
        var zero T
        return zero, false
    }
    return s.elements[len(s.elements)-1], true
}

// Comprobar si la pila está vacía
func (s *Stack[T]) IsEmpty() bool {
    return len(s.elements) == 0
}

// Ejemplo de uso
func main() {
    // Pila de enteros
    intStack := Stack[int]{}
    intStack.Push(1)
    intStack.Push(2)
    intStack.Push(3)
   
    fmt.Println(intStack.Pop())  // Salida: 3 true
   
    // Pila de cadenas
    stringStack := Stack[string]{}
    stringStack.Push("hello")
    stringStack.Push("world")
   
    fmt.Println(stringStack.Pop())  // Salida: world true
}

2. Mapas genéricos

Ejemplo

// Mapa genérico seguro para hilos
type SafeMap[K comparable, V any] struct {
    data map[K]V
    mutex sync.RWMutex
}

// Crear un nuevo SafeMap
func NewSafeMap[K comparable, V any]() *SafeMap[K, V] {
    return &SafeMap[K, V]{
        data: make(map[K]V),
    }
}

// Establecer un par clave-valor
func (m *SafeMap[K, V]) Set(key K, value V) {
    m.mutex.Lock()
    defer m.mutex.Unlock()
    m.data[key] = value
}

// Obtener un valor
func (m *SafeMap[K, V]) Get(key K) (V, bool) {
    m.mutex.RLock()
    defer m.mutex.RUnlock()
    value, exists := m.data[key]
    return value, exists
}

// Eliminar una clave
func (m *SafeMap[K, V]) Delete(key K) {
    m.mutex.Lock()
    defer m.mutex.Unlock()
    delete(m.data, key)
}

// Obtener todas las claves
func (m *SafeMap[K, V]) Keys() []K {
    m.mutex.RLock()
    defer m.mutex.RUnlock()
   
    keys := make([]K, 0, len(m.data))
    for key := range m.data {
        keys = append(keys, key)
    }
    return keys
}

// Ejemplo de uso
func main() {
    // Crear un mapa de cadena a entero
    scores := NewSafeMap[string, int]()
    scores.Set("Alice", 95)
    scores.Set("Bob", 87)
   
    if score, exists := scores.Get("Alice"); exists {
        fmt.Printf("Alice's score: %d\n", score)  // Salida: Alice's score: 95
    }
   
    fmt.Println("Keys:", scores.Keys())  // Salida: Keys: [Alice Bob]
}

Inferencia de tipos

El compilador de Go puede inferir automáticamente los parámetros de tipo, haciendo que el código sea más conciso.

Ejemplo

// No es necesario especificar el tipo explícitamente
func main() {
    // Ejemplo de inferencia de tipos
    fmt.Println(Max([]int{1, 2, 3}))        // El compilador infiere que T es int
    fmt.Println(Max([]float64{1.1, 2.2}))   // El compilador infiere que T es float64
   
    // Especificar el tipo explícitamente (a veces necesario)
    var result int = Max[int]([]int{1, 2, 3})
    fmt.Println(result)
}

Ejercicios prácticos

Ejercicio 1: Implementar un filtro genérico

Escribe unaFilterfunción que filtre los elementos de un slice según una condición.

Ejemplo

// Tu código de implementación aquí
func Filter[T any](slice []T, predicate func(T) bool) []T {
    // Implementa la lógica de filtrado
}

// Código de prueba
func main() {
    numbers := []int{1, 2, 3, 4, 5, 6}
    even := Filter(numbers, func(n int) bool {
        return n%2 == 0
    })
    fmt.Println(even)  // Debería imprimir: [2 4 6]
}

Ejercicio 2: Implementar una función de mapeo genérica

Escribe unaMapfunción que convierta cada elemento de un slice a otro tipo.

Ejemplo

// Tu código de implementación aquí
func Map[T any, U any](slice []T, mapper func(T) U) []U {
    // Implementa la lógica de mapeo
}

// Código de prueba
func main() {
    numbers := []int{1, 2, 3, 4, 5}
    strings := Map(numbers, func(n int) string {
        return fmt.Sprintf("Number: %d", n)
    })
    fmt.Println(strings)
}

Preguntas frecuentes y consideraciones

1. Consideraciones de rendimiento

Los genéricos realizan una especialización de tipos en tiempo de compilación, por lo que el rendimiento en tiempo de ejecución es equivalente al del código escrito a mano para tipos específicos.

2. Selección de restricciones de tipo

  • Usaranyes el más flexible, pero con funcionalidad limitada
  • Usarcomparableadmite comparación de igualdad
  • Usar restricciones de unión para limitar los tipos concretos disponibles

3. Manejo de errores

Ejemplo

// Buenas prácticas de manejo de errores
func SafeMax[T Number](slice []T) (T, error) {
    if len(slice) == 0 {
        var zero T
        return zero, errors.New("slice is empty")
    }
    return Max(slice), nil
}
Otras extensiones