Ordenamiento de estructuras en Go: cómo escribir de forma corta y no volverse loco
\n\nImagina: tienes un slice de estructuras y necesitas ordenarlo por cualquier campo. Nombre, edad, salario, fecha de nacimiento — lo que sea. En Stack Overflow, la pregunta «What is the shortest way to simply sort an array of structs by (arbitrary) field names?» ha acumulado miles de visitas. Y no es casualidad. En Go no existe un método «mágico» incorporado como list.sort(key=lambda x: x.field) de Python. Pero hay una forma elegante y potente: sort.Slice. En este artículo no solo mostraremos el código, sino que miraremos bajo el capó: cómo funciona el ordenamiento, qué hace el runtime y cómo evitar errores típicos.
Problema: «¿Por qué en Go no se puede simplemente escribir sort()?»
\n\nSi vienes de Python, JavaScript o Ruby, estás acostumbrado a una sola línea:
\nusers.sort(key=lambda u: u.age)\nEn Go no existe ese lujo. El tipado estricto y la ausencia de genéricos (hasta Go 1.18) obligaban a escribir una implementación separada de sort.Interface para cada tipo de estructura. Era engorroso. Por ejemplo:
type ByAge []User\nfunc (a ByAge) Len() int { return len(a) }\nfunc (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }\nfunc (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }\n\nfunc main() {\n users := []User{{"Alice", 30}, {"Bob", 25}}\n sort.Sort(ByAge(users))\n}\nFunciona, pero para cada campo — un nuevo tipo. ¿Y si hay 10 campos? El código se convierte en un lío. Uno quiere algo más compacto.
\n\nSolución: sort.Slice — tu kit de caballero
\n\nA partir de Go 1.8, el paquete sort incluye la función Slice. Esta toma un slice y una función anónima (cierre) que define el orden. Aquí está la forma más corta:
sort.Slice(users, func(i, j int) bool {\n return users[i].Age < users[j].Age\n})\nEso es todo. Tres líneas. Sin declarar nuevos tipos, sin métodos adicionales. ¿Quieres ordenar por nombre? Cambiamos el campo:
\nsort.Slice(users, func(i, j int) bool {\n return users[i].Name < users[j].Name\n})\n¿Y si es en orden descendente? Simplemente cambiamos el signo:
\nsort.Slice(users, func(i, j int) bool {\n return users[i].Age > users[j].Age // ahora descendente\n})\nHermoso. Pero ¿cómo funciona? ¿Y cuál es el truco?
\n\nBajo el capó: sort.Slice y funciones anónimas
\n\nAnalicemos qué sucede cuando llamas a sort.Slice. La función se declara así:
func Slice(slice interface{}, less func(i, j int) bool)\nLo primero que salta a la vista es interface{}. La función acepta cualquier slice. ¿Cómo sabe su longitud y cómo intercambia elementos? La respuesta es mediante reflection (paquete reflect).
Internamente, sort.Slice hace lo siguiente:
- \n
- Verifica que se haya pasado un slice. Si se pasa algo que no es un slice (por ejemplo, un array o una estructura), el programa entrará en pánico en tiempo de ejecución. \n
- Extrae la longitud mediante
reflect.ValueOf(slice).Len(). \n - Crea un objeto auxiliar que implementa la interfaz
sort.Interfacedinámicamente. Este objeto almacena una referencia al slice original (a través de reflection) y a tu funciónless. \n - Ejecuta el algoritmo de ordenamiento estándar (quicksort del paquete
sort). Cuando el algoritmo necesita comparar dos elementos, llama a tu funciónless. Cuando necesita intercambiarlos, usareflect.Swapper(slice). \n
El punto clave: tu función less captura el slice original del contexto externo. Esto es un cierre (closure). El compilador coloca la variable users en el heap para que esté disponible incluso después de salir de la función m