Ordenamiento de estructuras en Go: el camino corto y la magia bajo el capó

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

Ordenamiento de estructuras en Go: cómo escribir de forma corta y no volverse loco

\n\n

Imagina: 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.

\n\n

Problema: «¿Por qué en Go no se puede simplemente escribir sort()?»

\n\n

Si vienes de Python, JavaScript o Ruby, estás acostumbrado a una sola línea:

\n
users.sort(key=lambda u: u.age)
\n

En 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:

\n
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}
\n

Funciona, 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\n

Solución: sort.Slice — tu kit de caballero

\n\n

A 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:

\n
sort.Slice(users, func(i, j int) bool {\n    return users[i].Age < users[j].Age\n})
\n

Eso es todo. Tres líneas. Sin declarar nuevos tipos, sin métodos adicionales. ¿Quieres ordenar por nombre? Cambiamos el campo:

\n
sort.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:

\n
sort.Slice(users, func(i, j int) bool {\n    return users[i].Age > users[j].Age // ahora descendente\n})
\n

Hermoso. Pero ¿cómo funciona? ¿Y cuál es el truco?

\n\n

Bajo el capó: sort.Slice y funciones anónimas

\n\n

Analicemos qué sucede cuando llamas a sort.Slice. La función se declara así:

\n
func Slice(slice interface{}, less func(i, j int) bool)
\n

Lo 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).

\n

Internamente, sort.Slice hace lo siguiente:

\n
    \n
  1. 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.
  2. \n
  3. Extrae la longitud mediante reflect.ValueOf(slice).Len().
  4. \n
  5. Crea un objeto auxiliar que implementa la interfaz sort.Interface dinámicamente. Este objeto almacena una referencia al slice original (a través de reflection) y a tu función less.
  6. \n
  7. Ejecuta el algoritmo de ordenamiento estándar (quicksort del paquete sort). Cuando el algoritmo necesita comparar dos elementos, llama a tu función less. Cuando necesita intercambiarlos, usa reflect.Swapper(slice).
  8. \n
\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

Blogs

Book Recommendations