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
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
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
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
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
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
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
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
type NumericStringer interface {
Number
String() string
}
Práctica de funciones genéricas
1. Funciones de utilidad generales
Ejemplo
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
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
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
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
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
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
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
- Usar
anyes el más flexible, pero con funcionalidad limitada - Usar
comparableadmite comparación de igualdad - Usar restricciones de unión para limitar los tipos concretos disponibles
3. Manejo de errores
Ejemplo
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
}