Comparación de slices en Go: cómo no perder rendimiento y no volverse loco
\n\nImagine la situación: está escribiendo un servicio de alta carga en Go y necesita comparar dos slices de bytes o cadenas. Parecería que no podría ser más sencillo. Pero el diablo, como siempre, está en los detalles. En Stack Overflow, la pregunta "Checking the equality of two slices" ha acumulado miles de visitas, y no es casualidad. En este artículo, analizaremos qué trampas le esperan al desarrollador, cómo escribir una comparación correcta y, lo más importante, cómo hacerlo de la manera más rápida posible.
\n\nNo usaremos aburridas definiciones académicas. En su lugar, ejemplos prácticos, benchmarks y consejos prácticos que podrá aplicar hoy mismo. ¡Allá vamos!
\n\n¿Por qué una comparación simple no siempre es la correcta?
\n\nEn Go, un slice no es un array. Es un descriptor que contiene un puntero al array subyacente (underlying array), la longitud y la capacidad. Por lo tanto, el operador == no funciona para slices. Intentar escribir slice1 == slice2 provocará un error de compilación:
invalid operation: slice1 == slice2 (slice can only be compared to nil)\n\nEsto no es un error, sino una característica. Los desarrolladores de Go prohibieron intencionalmente la comparación directa de slices porque podría tener consecuencias no obvias. Por ejemplo, dos slices pueden hacer referencia al mismo array subyacente pero tener longitudes diferentes. O viceversa: arrays diferentes pero con los mismos datos.
\n\nEntonces, ¿qué hacer? Hay varios enfoques y analizaremos cada uno de ellos.
\n\nMétodo 1: Bucle manual (básico pero fiable)
\n\nLa forma más obvia es escribir una función que recorra todos los elementos y los compare uno por uno. Esto le da control total sobre el proceso.
\n\nfunc EqualSlices(a, b []int) bool {\n if len(a) != len(b) {\n return false\n }\n for i := range a {\n if a[i] != b[i] {\n return false\n }\n }\n return true\n}\n\nVentajas: transparencia, sin dependencias, posibilidad de personalización (por ejemplo, comparación con tolerancia para float).
\nDesventajas: hay que escribirlo para cada tipo de dato.
Método 2: reflect.DeepEqual (universal pero lento)
\n\nEn la biblioteca estándar existe la función reflect.DeepEqual, que puede comparar cualquier estructura de datos: slices, mapas, estructuras. Se ve así:
import "reflect"\n\nfunc main() {\n a := []int{1, 2, 3}\n b := []int{1, 2, 3}\n fmt.Println(reflect.DeepEqual(a, b)) // true\n}\n\nParecería ideal. Pero hay un detalle: reflect.DeepEqual usa reflexión, y eso siempre es costoso. En slices pequeños la diferencia es imperceptible, pero si tiene miles de elementos, el rendimiento caerá drásticamente.
Importante: reflect.DeepEqual también considera que nil y un slice vacío son diferentes. Esto puede ser una sorpresa:
var a []int = nil\nb := []int{}\nfmt.Println(reflect.DeepEqual(a, b)) // false!\n\nMétodo 3: bytes.Equal para slices de bytes (óptimo)
\n\nSi trabaja con []byte, la mejor opción es la función bytes.Equal de la biblioteca estándar. Está escrita en ensamblador y está altamente optimizada:
import "bytes"\n\nfunc main() {\n a := []byte("hello")\n b := []byte("hello")\n fmt.Println(bytes.Equal(a, b)) // true\n}\n\nEsta es la forma más rápida para slices de bytes. Internamente usa memcmp a nivel de C, lo que proporciona una enorme ganancia de rendimiento.
Método 4: Comparación con crypto/subtle (para criptografía)
\n\nMerece una mención especial el paquete crypto/subtle. Su función ConstantTimeCompare está diseñada para comparar datos secretos (contraseñas, tokens) sin fugas de tiempo. La comparación normal puede ser vulnerable a ataques de temporización (timing attacks):
import "crypto/subtle"\n\nfunc