Go

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ícate

Có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. Esto proporciona a los desarrolladores una herramienta poderosa para crear código modular y reutilizable.

Aprende Go GRATIS online

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.

Accede GRATIS a Go y certifícate

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.