¿Qué son los genéricos en Golang?

Go o Golang es un lenguaje de programación orientado a objetos, publicado por Google en 2009. Se caracteriza por ser compilado su soporte de concurrencia, basado en goroutines y canales y, precisamente esto último, lo convierte en una elección popular a la hora de crear sistemas distribuidos.

En primer lugar, es necesario hacer un breve apunte y es que, al contrario que otros lenguajes de programación orientados a objetos, Golang no dispone de herencias de tipo ni palabras clave (como la palabra class) que denoten el soporte de este paradigma.

Los genéricos, o programación genérica, es un concepto que se popularizó en la década de los 70, siendo Ada el lenguaje que lo llevó al público más amplio.

Cuando se utilizan genéricos, se emplean algoritmos que sean independientes de un único tipo de dato, siendo éste definido a posteriori. Gracias a esto, se permite la creación de funciones que operan con conjuntos de tipos de datos en vez de uno solo, reduciendo así la cantidad de código duplicado.

Funcionamiento de genéricos en Golang

Para comprender los genéricos en Golang, primero hay que entender las “constraints” que poseen. Estas “constraints” indican al parámetro correspondiente qué tipos son permitidos.

type Numeric interface {
	int | float64
}

func Sum[T int | float64](a, b T) T {
	return a + b
}

func Sum[T Numeric](a, b T) T {
	return a + b
}

Por ejemplo, en esta función tenemos un genérico T que tan solo permite dos tipos de datos: int y float64. También se observa que se pueden declarar los tipos directamente o crear una interfaz equivalente.

Otra cuestión que hay que tener en cuenta es que los tipos elegidos han de permitir todas las operaciones que se efectúen sobre el dato genérico.

func Multiply[T int | string](a, b T) T {
	return a * b
}

Un ejemplo de esto sería la función Multiply(). En este caso, el código no compilaría ya que el tipo string no se puede multiplicar.

Finalmente, las funciones no están restringidas a un solo tipo genérico, ya que pueden aceptar más de uno.

func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V {
	var s V
	for _, x := range m {
	s += x
}
return s
}

En este ejemplo se ven dos genéricos. El primero de ellos se llama K e implementa la interfaz comparable, mientras que el segundo, V, puede ser int64 o float64.

A modo de apunte, cuando se llama a una función con genéricos se puede especificar los tipos a la hora de llamarla o dejar que el compilador infiera estos tipos.

SumIntsOrFloats[string, int64](listOfInts)
SumIntsOrFloats(listOfInts)

Limitaciones en Golang

Por desgracia, al ser una adición relativamente nueva, Golang cuenta con una serie de limitaciones a la hora de usar genéricos.

  • No se pueden crear métodos genéricos sobre structs regulares, éstos han de ser genéricos.
// Uso no válido de los genéricos
type CustomStruct struct{...}

func (m CustomStruct) SomeMethod[T int | float64](x T) {
	...
}

// Uso válido de los genéricos
type CustomStruct[T] struct{
	value T
}

func (m CustomStruct[T]) SomeMethod(x T) {
	...
}
  • No hay parámetros variádicos.
    • Go no soporta actualmente parámetros de tipo variadic, lo que significa que no se puede definir una función o tipo genérico que acepte un número variable de argumentos de tipo.
// Esto no está permitido
func SumIntsOrFloats[V int64 | float64](nums ...V) V {
	var s V
	for _, x := range nums {
	s += x
}
return s
}
  • No se pueden hacer constantes genéricas.
    • Go no soporta el uso de constantes como parámetros de tipo. No se puede parametrizar un tipo con un valor constante, lo que podría ser útil en situaciones en las que se quieran crear tipos que dependan de valores constantes (por ejemplo, un tipo array de tamaño fijo).
  • No se pueden utilizar genéricos en interfaces.
// Esto no está permitido
type GenericInterface interface {
	GenericFunction[T int|float](num T) T
}

Ventajas y desventajas de Golang

Cada lenguaje que permita el uso de genéricos tiene sus ventajas y desventajas, así que en este apartado nos centraremos en analizarlas.

PROS

  • Reusabilidad y abstracción del código
    • Los genéricos permiten escribir código reutilizable independiente del tipado del código, traduciéndose en una menor cantidad de código duplicado.
  • Seguridad de tipo
    • Al limitar los posibles tipos que pueden adoptar una función, así se asegura que después los usuarios no utilicen tipos erróneos. Esto reduce los errores en tiempo de ejecución relacionados con type castings que normalmente ocurren cuando se usa interface{}.
  • Mejor rendimiento comparado a Interfaces vacías y Reflection
    • Como los genéricos se preparan en tiempo de compilación, brindan una mejora de rendimiento respecto a las interface{} y los reflects que se usan en Golang, ya que éstos se realizan en tiempo de ejecución y no están optimizados.

CONS

  • Incremento de complejidad
    • La filosofía de Go se basa en la simplicidad y, por desgracia, los genéricos añaden una capa de complejidad al código.
  • Binarios potencialmente más grandes
    • Como Go genera una versión específica de la función para cada genérico, puede llevar a la generación de binarios más pesados. Esto es un aspecto a tener en cuenta, ya que para sistemas más complejos puede llegar a incrementar el tamaño de los binarios generados.
  • Curva de aprendizaje
    • Este punto se puede ligar con el incremento de complejidad. Golang, al ser un lenguaje relativamente simple, esta capa de complejidad hace que a los desarrolladores más novatos les suponga una cierta dificultad aprender a utilizar este sistema de tipado más complejo.
  • Restricciones
    • Finalmente, los programadores se pueden ver limitados por las restricciones anteriormente mencionadas. Éstas hacen que ciertos usos de los genéricos que sí son posibles en otros lenguajes de programación, no se puedan dar con los genéricos de Go.
Conclusión

Los genéricos pueden ser una herramienta muy potente, no solo en Golang si no también en muchos otros lenguajes. Son capaces de reducir la cantidad de código duplicado y facilitar la vida a los desarrolladores, pero también cuentan con ciertas desventajas y limitaciones que se han de contemplar a la hora de ponerlos en práctica. Por lo tanto, a la hora de utilizarlos, hay que pensar bien en qué clase de solución estás implementado y si ésta se beneficiará de su uso.

Si este artículo te ha parecido interesante, te animamos a visitar la categoría Data Engineering para ver otros posts similares a este y a compartirlo en redes. ¡Hasta pronto!
Pere Alzamora
Pere Alzamora
Artículos: 6