ProgramaciónDesarrollador Go Medio

¿Cómo están estructurados los tipos de slices y arrays en Go? ¿Por qué es importante distinguir su semántica al pasarlos a funciones y trabajar con memoria?

Supere entrevistas con el asistente de IA Hintsage

Respuesta.

Los slices y arrays son una de las estructuras de datos más utilizadas en Go. A pesar de tener una sintaxis similar, la diferencia en su estructura y comportamiento puede llevar a errores de rendimiento, memoria y semántica.

Historia del tema:

Go desde el principio eligió un modelo explícito de gestión de memoria, donde los arrays son secuencias de elementos de tamaño fijo, mientras que los slices son una vista dinámica de un array. Esta división permite controlar el costo de las operaciones y el comportamiento del código.

Problema:

La principal dificultad es la confusión entre la copia de un array (semántica de valor) y la "referencia" de un slice. Los errores a menudo surgen al pasar estos tipos a funciones y al modificar valores, provocando efectos secundarios inesperados.

Solución:

Los arrays siempre se copian al pasarse por valor: la función recibe una copia de todo el contenido. Un slice, por otro lado, es una pequeña estructura (header) que contiene un puntero al array, la longitud y la capacidad. Los cambios dentro del slice son visibles externamente si se modifica el contenido del array (pero no si el slice se redirige a un nuevo array dentro de la función).

Ejemplo de código:

func updateArray(arr [3]int) { arr[0] = 10 } func updateSlice(slc []int) { slc[0] = 10 } func main() { a := [3]int{1,2,3} b := []int{1,2,3} updateArray(a) updateSlice(b) fmt.Println(a) // [1 2 3] fmt.Println(b) // [10 2 3] }

Características clave:

  • Array — tipo por valor, se copia completamente al ser pasado (el tamaño se compila en el tipo).
  • Slice — estructura envoltura: puntero al array, longitud y capacidad.
  • Eficiencia al pasar slices: la operación copia solo el header, no todo el contenido (pero los cambios internos son visibles desde todas las "vistas").

Preguntas capciosas.

¿Qué sucederá si cambias la longitud del slice dentro de la función? ¿Afectará esto al slice original?

No, cambiar la longitud del slice (por ejemplo, usando slc = slc[:2]) dentro de la función solo afectará a la copia local del header. El slice original permanecerá igual.

¿Devuelve el operador append el slice modificado en la misma área de memoria?

No necesariamente. Si no hay suficiente capacidad, se crea un nuevo array y se devuelve el puntero al nuevo array. El array antiguo permanecerá intacto.

Ejemplo de código:

s := []int{1,2,3} s2 := append(s, 4, 5, 6) // s2 puede estar en una nueva área de memoria

¿Se puede asignar un array a un slice o viceversa?

No. []int y [5]int son tipos diferentes. Para pasar un array como un slice, se debe usar la conversión arr[:]. Lo contrario no es posible.

Errores típicos y anti-patrones

  • Copiar un array y esperar que los cambios sean visibles desde fuera de la función.
  • Cambiar la longitud de un slice dentro de la función y esperar que esto se refleje fuera de la función.
  • Fugas de memoria a través de "arrays de respaldo" "largos" que almacenan un slice para pequeñas vistas.
  • Errores al usar append en un bucle, lo que podría crear nuevos arrays y dejar slices antiguos "colgando".

Ejemplo de la vida real

Caso negativo

Un desarrollador junior implementó una función para actualizar una tabla, pasando un array a la función esperando que los cambios se aplicaran al array original. Los cambios no "se guardaron".

Pros:

  • El código se leía y se probaba fácilmente en ejemplos pequeños.

Contras:

  • Errores en datos reales, dificultades de diagnóstico — el cambio era oculto.

Caso positivo

La función aceptaba un slice y devolvía claramente una copia modificada, aumentando la previsibilidad del efecto. Todos los cambios fueron conscientes, los datos no "se filtraron" ni se modificaron implícitamente.

Pros:

  • Sencillez y previsibilidad del comportamiento.
  • No hay "magia" con la copia o el cambio.

Contras:

  • Es necesario recordar cuándo y dónde se pasan los punteros y los slices para no conservar memoria innecesaria (array de respaldo).