Go 1.18 introdujo los genéricos como la mayor ampliación del lenguaje desde su creación. Los type parameters permiten escribir funciones y tipos que funcionan con cualquier tipo que cumpla ciertas restricciones, eliminando la necesidad de duplicar código o usar interface{} con type assertions en tiempo de ejecución.
Sintaxis básica: type parameters
Los type parameters van entre corchetes después del nombre de la función o tipo. El constraint define qué métodos u operaciones puede usar el código genérico:
package main
import "fmt"
// T puede ser cualquier tipo comparable
func Contiene[T comparable](slice []T, elem T) bool {
for _, v := range slice {
if v == elem {
return true
}
}
return false
}
func main() {
nums := []int{1, 2, 3, 4, 5}
palabras := []string{"hola", "mundo", "go"}
fmt.Println(Contiene(nums, 3)) // true
fmt.Println(Contiene(palabras, "go")) // true
// El compilador infiere el tipo en la llamada
// Equivalente explícito: Contiene[int](nums, 3)
}
Constraints: any, comparable y constraints.Ordered
Un constraint es una interfaz que define qué tipos puede tomar el parámetro. Go 1.18 incluye any (alias de interface{}), comparable y el paquete golang.org/x/exp/constraints:
import "golang.org/x/exp/constraints"
// constraints.Ordered incluye todos los tipos con operadores <, >, etc.
func Min[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
func Max[T constraints.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
func main() {
fmt.Println(Min(3, 7)) // 3
fmt.Println(Min(3.14, 2.71)) // 2.71
fmt.Println(Min("hola", "go")) // go (orden lexicográfico)
}
Constraints propios con interfaces de unión
Puedes definir tu propio constraint usando el operador | para listar los tipos permitidos:
// Número incluye todos los tipos numéricos integrados
type Número interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64
}
func Suma[T Número](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(Suma([]int{1, 2, 3, 4, 5})) // 15
fmt.Println(Suma([]float64{1.1, 2.2, 3.3})) // 6.6000...
}
Funciones genéricas: Map, Filter y Reduce
func Map[T, U any](slice []T, fn func(T) U) []U {
resultado := make([]U, len(slice))
for i, v := range slice {
resultado[i] = fn(v)
}
return resultado
}
func Filter[T any](slice []T, fn func(T) bool) []T {
var resultado []T
for _, v := range slice {
if fn(v) {
resultado = append(resultado, v)
}
}
return resultado
}
func Reduce[T, U any](slice []T, inicial U, fn func(U, T) U) U {
acc := inicial
for _, v := range slice {
acc = fn(acc, v)
}
return acc
}
func main() {
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
pares := Filter(nums, func(n int) bool { return n%2 == 0 })
cuadrados := Map(pares, func(n int) int { return n * n })
suma := Reduce(cuadrados, 0, func(acc, n int) int { return acc + n })
fmt.Println(pares) // [2 4 6 8 10]
fmt.Println(cuadrados) // [4 16 36 64 100]
fmt.Println(suma) // 220
}
Structs genéricos: pila tipada
type Pila[T any] struct {
elementos []T
}
func (p *Pila[T]) Push(v T) {
p.elementos = append(p.elementos, v)
}
func (p *Pila[T]) Pop() (T, bool) {
var cero T
if len(p.elementos) == 0 {
return cero, false
}
n := len(p.elementos) - 1
v := p.elementos[n]
p.elementos = p.elementos[:n]
return v, true
}
func (p *Pila[T]) Len() int {
return len(p.elementos)
}
func main() {
var pilaInt Pila[int]
pilaInt.Push(1)
pilaInt.Push(2)
pilaInt.Push(3)
for pilaInt.Len() > 0 {
v, _ := pilaInt.Pop()
fmt.Println(v) // 3, 2, 1
}
var pilaStr Pila[string]
pilaStr.Push("hola")
pilaStr.Push("go")
v, _ := pilaStr.Pop()
fmt.Println(v) // go
}
Antes de los genéricos, la única opción era usar interface{} y hacer type assertions en tiempo de ejecución, con el coste en rendimiento y seguridad que eso implica. Los genéricos resuelven esto en tiempo de compilación y el compilador genera código especializado para cada tipo concreto.
