Go
Tutorial Go: Generics
Go Generics en la programación orientada a objetos. Aprende a implementar funciones y tipos genéricos eficaces en Go para mejorar tu código.
Aprende Go GRATIS y certifícateCómo definir funciones y tipos genéricos
En Go, Los genéricos permite definir funciones y tipos que trabajan con cualquier tipo de dato, proporcionando una flexibilidad significativa al escribir código reutilizable. Para definir una función genérica, se utiliza un parámetro de tipo en la declaración de la función. Este se especifica dentro de corchetes []
después del nombre de la función y antes de los parámetros regulares.
Por ejemplo, una función genérica para intercambiar dos valores podría definirse así:
func Swap[T any](a, b T) (T, T) {
return b, a
}
En este ejemplo, T
es un parámetro de tipo que puede ser sustituido por cualquier tipo concreto cuando se llama a la función, y any
es una restricción de tipo que indica que T
puede ser cualquier tipo. La palabra clave any
es un sinónimo de interface{}
en Go, pero se prefiere por su claridad semántica en el contexto de los genéricos.
La definición de tipos genéricos en Go sigue un patrón similar al de las funciones genéricas. Un tipo genérico se declara utilizando parámetros de tipo en su definición. Por ejemplo, se puede definir una estructura genérica para una pareja de valores de cualquier tipo de la siguiente manera:
type Pair[T any] struct {
First, Second T
}
En este caso, Pair
es un tipo genérico que puede contener dos valores del mismo tipo T
. Al instanciar Pair
, se debe especificar el tipo concreto que sustituirá a T
. Por ejemplo:
pair := Pair[int]{First: 1, Second: 2}
Aquí, Pair[int]
indica que T
es sustituido por int
, por lo que First
y Second
son ambos de tipo entero.
Los genéricos en Go también permiten definir métodos para tipos genéricos. Un método se asocia a un tipo genérico de manera similar a como se hace con tipos no genéricos. Si continuamos con el ejemplo de Pair
, se podría definir un método que intercambie los valores First
y Second
:
func (p *Pair[T]) Swap() {
p.First, p.Second = p.Second, p.First
}
Este método Swap
puede ser llamado en cualquier instancia de Pair
, independientemente del tipo concreto que se haya utilizado para T
.
Es importante tener en cuenta que los genéricos en Go están diseñados para ser lo más eficientes posible y no introducen sobrecargas significativas en tiempo de ejecución. Sin embargo, su uso debe ser justificado por la necesidad de abstracción y reutilización del código. En situaciones donde los tipos concretos son suficientes, el uso de genéricos puede ser innecesario y complicar la lectura del código.
Restricciones de tipos mediante type constraints.
En Go 1.23.2, los type constraints son una característica esencial para trabajar con genéricos, ya que permiten definir restricciones sobre los tipos que pueden ser utilizados como argumentos de tipo en funciones y tipos genéricos. Estas restricciones se especifican mediante interfaces que describen un conjunto de métodos que un tipo debe implementar para ser considerado válido como argumento de tipo.
Para definir un type constraint, se utiliza una interfaz que actúa como un contrato. Esta interfaz puede ser una interfaz existente o una nueva interfaz creada específicamente para funcionar como constraint. Por ejemplo, si queremos restringir un tipo a aquellos que implementan el método String() string
, podemos definir una interfaz de la siguiente manera:
type Stringer interface {
String() string
}
func Imprimir[T Stringer](v T) {
fmt.Println(v.String())
}
En este ejemplo, Stringer
es un type constraint que asegura que cualquier tipo utilizado como argumento de tipo para la función Imprimir
debe implementar el método String() string
. Esto garantiza que Imprimir
solo se pueda invocar con tipos que cumplan con esta restricción, proporcionando seguridad de tipos en tiempo de compilación.
Es posible combinar múltiples restricciones en un solo type constraint utilizando la composición de interfaces. Si se desea que un tipo cumpla con varios contratos, se pueden combinar las interfaces requeridas. Por ejemplo:
type LectorEscritor interface {
io.Reader
io.Writer
}
func TransferirDatos[T LectorEscritor](destino, fuente T) error {
_, err := io.Copy(destino, fuente)
return err
}
En este caso, LectorEscritor
es un type constraint que requiere que un tipo implemente tanto las interfaces io.Reader
como io.Writer
. La función TransferirDatos
puede operar con cualquier tipo que cumpla con ambas interfaces, lo que la hace flexible y segura.
Otra característica útil de los type constraints es la posibilidad de restringir los tipos a un conjunto específico de tipos subyacentes mediante el uso de operadores. Esto es especialmente útil cuando se desea limitar el tipo a ciertos tipos primitivos, como números enteros o flotantes. Por ejemplo, para restringir a tipos numéricos enteros, se puede definir un constraint así:
type Entero interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}
func SumarEnteros[T Entero](a, b T) T {
return a + b
}
Aquí, Entero
es un type constraint que permite solo tipos numéricos enteros específicos. La función SumarEnteros
puede operar con cualquier tipo que cumpla con esta restricción, asegurando que solo se utilicen tipos numéricos adecuados.
El uso de type constraints en Go no solo proporciona seguridad de tipos, sino que también mejora la expresividad del código, permitiendo que las funciones y tipos genéricos sean más precisos en cuanto a los tipos que pueden manejar. Esto es crucial para desarrollar aplicaciones robustas y mantenibles, ya que evita errores comunes relacionados con incompatibilidades de tipos y permite optimizar el comportamiento de las funciones y tipos genéricos.
Ejemplos de uso de genéricos en estructuras de datos y funciones comunes
Los genéricos en Go permiten crear estructuras de datos y funciones que pueden operar con cualquier tipo de datos, proporcionando así una mayor abstracción y capacidad de reutilización. Uno de los usos más comunes de los genéricos es en la implementación de estructuras de datos como listas, pilas y colas, donde el tipo de los elementos es genérico y puede ser determinado durante la instancia.
Considera la implementación de una lista simple. En lugar de definir una lista para cada tipo de dato, se puede crear una lista genérica que acepte cualquier tipo:
type List[T any] struct {
items []T
}
func (l *List[T]) Add(item T) {
l.items = append(l.items, item)
}
func (l *List[T]) Get(index int) T {
return l.items[index]
}
En este ejemplo, List
es una estructura genérica que utiliza un slice para almacenar elementos del tipo T
. Los métodos Add
y Get
permiten agregar y recuperar elementos de la lista, respectivamente. Esta implementación genérica es eficiente y flexible, permitiendo el uso de List
con cualquier tipo de dato.
Otra aplicación común es la implementación de funciones de utilidad que operan sobre colecciones de datos. Por ejemplo, una función genérica para encontrar el máximo valor en un slice:
type Ordered interface {
~int | ~float64 | ~string
}
func Max[T Ordered](slice []T) T {
max := slice[0]
for _, v := range slice {
if v > max {
max = v
}
}
return max
}
Aquí, Max
es una función genérica que toma un slice de cualquier tipo que implemente la interfaz Ordered
, lo que asegura que los elementos pueden ser comparados con el operador >
. La función recorre el slice y devuelve el valor máximo. Este enfoque genérico elimina la necesidad de duplicar la lógica para cada tipo de dato.
Otro uso común de los genéricos es en funciones que operan sobre colecciones, como una función para filtrar elementos de una lista. La función puede ser escrita de manera que acepte cualquier tipo de lista y un criterio de filtrado definido por el usuario.
func Filter[T any](list []T, predicate func(T) bool) []T {
var result []T
for _, v := range list {
if predicate(v) {
result = append(result, v)
}
}
return result
}
En este caso, la función Filter
toma una lista de cualquier tipo y una función predicate
que define el criterio de filtrado. La función devuelve una nueva lista con los elementos que cumplen con el criterio, demostrando la flexibilidad de los genéricos para trabajar con diversas colecciones.
Los genéricos también son útiles para implementar estructuras de datos más complejas, como una lista enlazada genérica. Una lista enlazada es una estructura de datos donde cada elemento apunta al siguiente, permitiendo inserciones y eliminaciones eficientes.
type Node[T any] struct {
value T
next *Node[T]
}
type LinkedList[T any] struct {
head *Node[T]
}
func (l *LinkedList[T]) Add(value T) {
newNode := &Node[T]{value: value, next: l.head}
l.head = newNode
}
func (l *LinkedList[T]) Delete() {
if l.head == nil {
return
}
l.head = l.head.next
}
En este ejemplo, LinkedList
es una estructura de datos que puede almacenar cualquier tipo T
. Las funciones Add
y Delete
permiten añadir y quitar elementos de la lista de forma genérica.
El uso de genéricos en Go no solo simplifica la implementación de estructuras de datos y funciones comunes, sino que también mejora la seguridad y la robustez del código al asegurar que las operaciones se realizan solo sobre tipos compatibles.
Ejercicios de esta lección Generics
Evalúa tus conocimientos de esta lección Generics con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Cadenas de texto y manipulación
Selectores y mutexes: concurrencia y exclusión
Agenda de contactos por consola
Composición de structs en lugar de herencia
Estructuras de control
Arrays y slices
Control de flujo y estructuras de bucle
Sistema API REST gestión de libros
Métodos con receptores por valor y por puntero
API REST con net/http
Generics
Evaluación Go
Métodos HTTP con net/http
Crear e invocar funciones
Operadores y expresiones
Polimorfismo a través de Interfaces
Manejo explícito de errores
Estructuras structs
Tipos de datos, variables y constantes
Introducción a Go
Canales y comunicación entre Goroutines
Condiciones de carrera
Punteros y referencias
Goroutines y concurrencia básica
Instalación Go primer programa
Errores personalizados y trazabilidad
Estructuras de datos Mapas
Cliente de API OpenWeatherMap clima
Todas las lecciones de Go
Accede a todas las lecciones de Go y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Go
Introducción Y Entorno
Instalación Y Primer Programa De Go
Introducción Y Entorno
Tipos De Datos, Variables Y Constantes
Sintaxis
Operadores Y Expresiones
Sintaxis
Cadenas De Texto Y Manipulación
Sintaxis
Estructuras De Control
Sintaxis
Control De Flujo Y Estructuras De Bucle
Sintaxis
Funciones
Sintaxis
Arrays Y Slices
Estructuras De Datos
Mapas
Estructuras De Datos
Punteros Y Referencias
Estructuras De Datos
Estructuras Structs
Programación Orientada A Objetos
Métodos Con Receptores Por Valor Y Por Puntero
Programación Orientada A Objetos
Polimorfismo A Través De Interfaces
Programación Orientada A Objetos
Composición De Structs En Lugar De Herencia
Programación Orientada A Objetos
Generics
Programación Orientada A Objetos
Manejo Explícito De Errores
Manejo De Errores Y Excepciones
Errores Personalizados Y Trazabilidad
Manejo De Errores Y Excepciones
Métodos Http Con Net/http
Comunicación Por Http
Api Rest Con Net/http
Comunicación Por Http
Goroutines Y Concurrencia Básica
Concurrencia Y Paralelismo
Canales Y Comunicación Entre Goroutines
Concurrencia Y Paralelismo
Condiciones De Carrera
Concurrencia Y Paralelismo
Selectores Y Mutexes Concurrencia Y Exclusión Mutua
Concurrencia Y Paralelismo
Evaluación Conocimientos Go
Evaluación
Certificados de superación de Go
Supera todos los ejercicios de programación del curso de Go y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender la sintaxis y el uso de genéricos en Go.
- Definir funciones y tipos genéricos utilizando parámetros de tipo.
- Implementar type constraints para restringir tipos en genéricos.
- Aplicar genéricos en estructuras de datos y funciones comunes.
- Mejorar la reutilización y abstracción del código usando genéricos.
- Utilizar genéricos para asegurar la seguridad y robustez del código.