Generics en Go
Go 1.18 introdujo generics (polimorfismo parametrico), permitiendo escribir funciones y tipos que operan sobre multiples tipos sin sacrificar la seguridad de tipos en compilacion.
Antes de Generics
Sin generics, las opciones eran usar interface{} (perdiendo tipo) o duplicar codigo:
// Duplicacion: una funcion por tipo
func SumaInts(nums []int) int {
var total int
for _, n := range nums {
total += n
}
return total
}
func SumaFloat64s(nums []float64) float64 {
var total float64
for _, n := range nums {
total += n
}
return total
}
Generics resuelve esto con type parameters.
Type Parameters
Un type parameter se declara entre corchetes [T constraint] antes de los parametros regulares:
package main
import "fmt"
func Suma[T int | float64](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
func main() {
enteros := []int{1, 2, 3, 4, 5}
flotantes := []float64{1.1, 2.2, 3.3}
fmt.Println(Suma(enteros)) // 15
fmt.Println(Suma(flotantes)) // 6.6
// Tipo explicito (opcional si Go puede inferirlo)
fmt.Println(Suma[int](enteros)) // 15
}
Go infiere el tipo automaticamente en la mayoria de los casos.
Constraints
Las constraints definen que operaciones soporta un type parameter.
Constraints Incorporados
| Constraint | Descripcion |
|---|---|
any | Cualquier tipo (alias de interface{}) |
comparable | Tipos que soportan == y != |
func Contiene[T comparable](slice []T, objetivo T) bool {
for _, v := range slice {
if v == objetivo {
return true
}
}
return false
}
func main() {
fmt.Println(Contiene([]string{"a", "b", "c"}, "b")) // true
fmt.Println(Contiene([]int{1, 2, 3}, 5)) // false
}
Constraints como Interfaces
Puedes definir constraints personalizados usando interfaces con type lists:
type Numero interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~float32 | ~float64
}
func Min[T Numero](a, b T) T {
if a < b {
return a
}
return b
}
func main() {
fmt.Println(Min(3, 7)) // 3
fmt.Println(Min(2.5, 1.8)) // 1.8
}
El operador ~ indica tipos subyacentes: ~int incluye int y cualquier tipo definido como type MiEntero int.
Constraints con Metodos
Las constraints pueden combinar type lists y metodos:
type Stringer interface {
~int | ~string
String() string
}
Esto restringe a tipos cuyo tipo subyacente sea int o string y que implementen String().
Paquete constraints (golang.org/x/exp)
El paquete experimental constraints provee constraints utiles predefinidos:
| Constraint | Incluye |
|---|---|
Signed | int, int8, int16, int32, int64 |
Unsigned | uint, uint8, uint16, uint32, uint64, uintptr |
Integer | Signed + Unsigned |
Float | float32, float64 |
Complex | complex64, complex128 |
Ordered | Integer + Float + ~string |
import "golang.org/x/exp/constraints"
func Max[T constraints.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
Desde Go 1.21, el paquete cmp de la stdlib incluye cmp.Ordered:
import "cmp"
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
Funciones Genericas
Multiples Type Parameters
func Map[T any, R any](slice []T, fn func(T) R) []R {
result := make([]R, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
func main() {
nums := []int{1, 2, 3, 4}
strs := Map(nums, func(n int) string {
return fmt.Sprintf("#%d", n)
})
fmt.Println(strs) // [#1 #2 #3 #4]
}
Filter Generico
func Filter[T any](slice []T, pred func(T) bool) []T {
var result []T
for _, v := range slice {
if pred(v) {
result = append(result, v)
}
}
return result
}
func main() {
nums := []int{1, 2, 3, 4, 5, 6}
pares := Filter(nums, func(n int) bool {
return n%2 == 0
})
fmt.Println(pares) // [2 4 6]
}
Tipos Genericos
Los structs tambien pueden ser genericos:
package main
import "fmt"
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 zero T
if len(p.elementos) == 0 {
return zero, false
}
ultimo := p.elementos[len(p.elementos)-1]
p.elementos = p.elementos[:len(p.elementos)-1]
return ultimo, true
}
func (p *Pila[T]) Len() int {
return len(p.elementos)
}
func main() {
pila := &Pila[int]{}
pila.Push(10)
pila.Push(20)
pila.Push(30)
for pila.Len() > 0 {
v, _ := pila.Pop()
fmt.Println(v) // 30, 20, 10
}
}
Resultado Generico (Result Type)
type Resultado[T any] struct {
Valor T
Err error
}
func NewOk[T any](v T) Resultado[T] {
return Resultado[T]{Valor: v}
}
func NewErr[T any](err error) Resultado[T] {
return Resultado[T]{Err: err}
}
func (r Resultado[T]) Unwrap() (T, error) {
return r.Valor, r.Err
}
Paquetes slices y maps
Go 1.21+ incluye paquetes genericos en la stdlib:
package main
import (
"fmt"
"slices"
)
func main() {
nums := []int{3, 1, 4, 1, 5, 9}
// Ordenar
slices.Sort(nums)
fmt.Println(nums) // [1 1 3 4 5 9]
// Buscar
idx, found := slices.BinarySearch(nums, 4)
fmt.Println(idx, found) // 3 true
// Contiene
fmt.Println(slices.Contains(nums, 5)) // true
// Compactar (eliminar duplicados consecutivos)
nums = slices.Compact(nums)
fmt.Println(nums) // [1 3 4 5 9]
// Reversar
slices.Reverse(nums)
fmt.Println(nums) // [9 5 4 3 1]
}
package main
import (
"fmt"
"maps"
)
func main() {
m1 := map[string]int{"a": 1, "b": 2}
m2 := map[string]int{"b": 3, "c": 4}
// Copiar todos los pares de m2 a m1
maps.Copy(m1, m2)
fmt.Println(m1) // map[a:1 b:3 c:4]
// Obtener claves
for k := range maps.Keys(m1) {
fmt.Print(k, " ")
}
// Comparar igualdad
fmt.Println(maps.Equal(m1, m2)) // false
}
Cuando Usar Generics
| Escenario | Solucion |
|---|---|
| Estructuras de datos (pila, cola, arbol) | Generics |
| Funciones utilitarias (map, filter, reduce) | Generics |
| Comportamiento polimorfico (distintas implementaciones) | Interfaces |
| Operaciones sobre un tipo concreto | Funciones regulares |
| Serializacion/metaprogramacion | Reflection |
Regla practica: si escribes la misma logica para tipos distintos y solo cambia el tipo, usa generics. Si el comportamiento cambia, usa interfaces.
Limitaciones Actuales
| Limitacion | Estado |
|---|---|
| No hay metodos genericos en tipos no genericos | No soportado |
| No hay especializacion de tipos | No soportado |
| No hay type parameters en metodos | No soportado |
| No hay operador ternario generico | No aplica en Go |
Constraints no pueden ser tipos concretos sin ~ | Por diseno |
type MiTipo struct{}
// ERROR: los metodos no pueden tener type parameters propios
// func (m MiTipo) Hacer[T any](v T) {}
// SOLUCION: usar una funcion libre
func Hacer[T any](m MiTipo, v T) {}
// O hacer el tipo generico
type MiTipo2[T any] struct{ valor T }
func (m MiTipo2[T]) Hacer() T { return m.valor }
Resumen
| Concepto | Sintaxis |
|---|---|
| Funcion generica | func F[T constraint](param T) T |
| Tipo generico | type Nombre[T constraint] struct{} |
| Constraint union | interface{ ~int | ~string } |
| any | Cualquier tipo |
| comparable | Soporta == y != |
| cmp.Ordered | Soporta <, >, <=, >= |
| Paquete slices | Ordenar, buscar, filtrar slices |
| Paquete maps | Copiar, comparar, iterar maps |
← Capitulo 20: Build y Deploy | Capitulo 22: Proyecto API REST →