Introducción
En el artículo anterior vimos los arrays en Go: colecciones de tamaño fijo cuya longitud forma parte del tipo. Esa rigidez los hace seguros y predecibles, pero poco prácticos para la mayoría de situaciones reales, cuando necesitamos colecciones que crezcan, se reduzcan o se manipulen con flexibilidad.
Go no elimina los arrays. Los mantiene como bloque básico y sobre ellos construye la estructura que realmente se usa en el día a día: los slices.
En este artículo vas a entender qué es un slice, cómo se crea, cómo se utiliza y qué consecuencias tiene su diseño interno (memoria compartida, rendimiento y efectos colaterales).
El problema de los arrays
Recuerda el ejemplo que vimos en el artículo sobre arrays:
var small [3]int
var big [4]int
small = big // Error: cannot use big (variable of type [4]int) as [3]int value in assignment
Los arrays tienen limitaciones:
- Su tamaño es fijo y no puede cambiar (ni crecer ni reducirse).
- La longitud forma parte del tipo (por tanto, dos arrays de distinto tamaño no son compatibles).
La mayoría de programas necesitan listas que crecen o se construyen progresivamente. Para eso, Go introduce los slices.
Qué es un slice
En Go, un slice es una estructura ligera que describe una vista dinámica sobre un array subyacente conocido como backing array. Esta estructura está compuesta por tres elementos:
- Puntero al primer elemento visible del backing array.
- Longitud (len): número de elementos accesibles desde ese puntero.
- Capacidad (cap): cuántos elementos caben desde ese puntero hasta el final del backing array (antes de necesitar otro backing array).
El slice no almacena los datos: describe qué porción del backing array es visible y cuánto puede crecer dentro de ese mismo almacenamiento.
Entender esto es clave para razonar correctamente sobre comportamiento, efectos colaterales y crecimiento posterior.
Crear un slice
Un slice puede crearse aplicando la sintaxis de slicing a un array o a otro slice. La regla general es esta:
view := x[inicio:fin]
Donde x puede ser:
- un array
- un slice
Y donde:
- Se incluye el elemento
inicio. - Se excluye el elemento
fin. - Si no pones
inicio, se asume queiniciovale 0. - Si no pones
fin, se asume quefintoma como valor la longitud del array.
Esta sintaxis no crea datos nuevos. Crea un slice llamado view que actúa como vista sobre una parte de x.
Dado este array:
data := [10]int{0,10,20,30,40,50,60,70,80,90}
Veamos algunos ejemplos:
Ejemplo 1
Creamos un slice window que apunta a los elementos 3, 4, 5, y 6 del array:
window := data[3:7]
fmt.Println(window)
[30 40 50 60]
Fíjate que:
windowincluye el índice 3 del array.windowexcluye el índice 7 del array.
Ejemplo 2
Creamos un slice head con los 5 primeros elementos:
head := data[:5]
fmt.Println(head)
[0 10 20 30 40]
Ejemplo 3
Creamos un slice tail desde el índice 5 hasta el final:
tail := data[5:]
fmt.Println(tail)
[50 60 70 80 90]
Ejemplo 4
Creamos un slice all con todo el array como slice:
all := data[:]
fmt.Println(all)
[0 10 20 30 40 50 60 70 80 90]
Hasta aquí, lo único que necesitas interiorizar es esto:
datacontiene los datos.window,head,tail,allson distintas vistas.- Si modificas elementos visibles desde una vista, estás modificando el mismo almacenamiento.
Anatomía de un slice
Para entender por qué ocurre esto, necesitamos mirar la anatomía interna de un slice.
El backing array
El backing array es el array donde se guardan los datos. En el ejemplo anterior, data es el backing array.
Puntero al primer elemento
El slice guarda un puntero al primer elemento visible dentro del backing array.
Cuando modificas algún elemento del slice, en realidad estás modificando un componente del backing array. Considera este ejemplo:
alias := window
alias[0] = 99
fmt.Println("window:", window)
fmt.Println("alias:", alias)
fmt.Println("data:", data)
Resultado:
window: [99 40 50 60]
alias: [99 40 50 60]
data: [0 10 20 99 40 50 60 70 80 90]
¿Qué ha ocurrido?
windowyaliasson dos variables de tipo slice distintas.- Pero ambas contienen un puntero que apunta al mismo backing array.
- Al modificar
alias[0], estamos modificando el mismo almacenamiento que vewindow.
La longitud (len)
La longitud indica cuántos elementos son visibles a través del slice. Se calcula con la función len():
fmt.Println(len(window))
4
Estos son los 4 elementos visibles:
[99 40 50 60]
Los índices válidos de window son 0, 1, 2 y 3 (de 0 a len(window)-1).
La capacidad (cap)
La capacidad indica cuántos elementos caben desde el inicio del slice hasta el último elemento del backing array. Se calcula con la función cap():
fmt.Println(cap(window))
7
En nuestro ejemplo, el slice empieza en el índice 3 del backing array, y termina en el índice 9. Desde el índice 3 hasta el final del array (índice 9) hay en total 7 posiciones posibles.
Representación conceptual
La situación se entiende conceptualmente de esta manera:
backing array: [0 10 20 99 40 50 60 70 80 90]
↑ ↑
inicio fin
Slice window = [99 40 50 60]
len(window) = 4
cap(window) = 7
Añadir elementos
Partimos de un slice ìtems que todavía tiene espacio disponible:
buf := [10]int{0, 10, 20, 99, 40, 50, 60, 70, 80, 90}
items := buf[:5]
fmt.Println(items)
[0 10 20 99 40]
En este momento:
len(items) = 5
cap(items) = 10
El slice expone 5 elementos, pero su backing array tiene 10 posiciones desde el inicio de items, así que puede crecer 5 más sin realocar.
Añadir un elemento
Para añadir un nuevo elemento a un slice se utiliza la función append() de esta manera:
items = append(items, 100)
fmt.Println(items)
Resultado:
[0 10 20 99 40 100]
Ahora:
len(items) = 6
cap(items) = 10
El nuevo elemento se ha añadido en el mismo backing array. No ha sido necesario reservar memoria adicional.
Añadiendo varios elementos
Para añadir varios elementos a un slice se utiliza la función append() de esta manera:
items = append(items, 200, 300)
fmt.Println(items)
Resultado:
[0 10 20 99 40 100 200 300]
Mientras exista capacidad disponible, append() simplemente utiliza el espacio libre. La operación es muy eficiente, no necesita reservar memoria adicional.
Cuando el slice se queda sin espacio
Si añades más elementos de los que caben en cap, Go necesitará un backing array nuevo:
items = append(items, 400, 500, 600)
fmt.Println(items)
[0 10 20 99 40 100 200 300 400 500 600]
En este momento Go:
- crea un nuevo backing array con mayor capacidad
- copia los elementos existentes
- hace que
itemsapunte a ese nuevo backing array
Este proceso se denomina reasignación de memoria (en inglés, reallocation).
El cambio es transparente en el código, pero crucial para entender efectos colaterales con otros slices.
Slices que comparten memoria
Cuando append() realoca, el descriptor que devuelve pasa a apuntar a un backing array nuevo. Los demás slices siguen apuntando al original.
base := [4]int{1, 2, 3, 4}
left := base[:2] // [1 2], cap=4
mid := base[1:3] // [2 3], cap=3 (mismo backing array)
left = append(left, 99, 88, 77) // fuerza crecimiento (no cabe en cap=4)
fmt.Println("left:", left) // apunta al nuevo array
fmt.Println("mid:", mid) // sigue apuntando al array original
fmt.Println("base:", base)
El resultado es este:
left: [1 2 99 88 77]
mid: [2 3]
base: [1 2 3 4]
Aquí:
leftapunta al nuevo backing array. Ya no comparte backing array conmid/base.midybasesiguen compartiendo el backing array original.
Este comportamiento permite que slices crezcan dinámicamente sin romper a otros, pero también explica por qué a veces hay bugs cuando no hay realocación y append() pisa memoria compartida. Esto ocurre cuando aún hay capacidad disponible y append() reutiliza el mismo backing array.
Reservar capacidad
Cuando sabes que un slice va a crecer, es buena práctica reservar capacidad por adelantado usando la función make(), para reducir realocaciones. Esta función tiene tres parámetros:
- El tipo de slice (
[]int). - La longitud inicial (
len) - La capacidad (opcional). Si no lo pones, toma el valor de
len.
Ejemplo:
items1 := make([]int, 0, 100) // len=0, cap=100 (ideal para ir añadiendo)
items2 := make([]int, 5) // len=5, cap=5 (5 ceros)
items3 := make([]int, 3, 10) // len=3, cap=10
Esto no cambia el comportamiento del slice, pero si puede mejorar el rendimiento de la aplicación al evitar crear arrays nuevos de forma innecesaria.
Copiar un slice
La forma estándar de copiar elementos desde un slice origen hacia un slice destino es mediante la función copy().
n := copy(dst, src)
Esta función copia elementos desde src a dst y devuelve cuántos elementos ha copiado.
Reglas importantes:
- Copia hasta el mínimo entre
len(dst)ylen(src). - Copia elementos en orden (como un “memcpy” conceptual a nivel de elementos).
- No cambia ni len ni cap de ninguno de los slices.
- Solo copia los elementos visibles (la parte accesible por len).
Esta función es especialmente útil cuando necesitas una copia independiente para evitar efectos colaterales derivados de que varios slices compartan el mismo backing array.
Copia independiente típica
Si quieres una copia completa e independiente de src, primero creas un dst con la misma longitud y luego copias:
src := []int{1, 2, 3}
dst := make([]int, len(src))
copy(dst, src)
dst[0] = 99
fmt.Println("src:", src) // src: [1 2 3]
fmt.Println("dst:", dst) // dst: [99 2 3]
Aquí dst tiene su propio backing array, así que no hay efectos colaterales.
Copia parcial (cuando dst es más corto)
En este ejemplo estamos copiando un slice más grande (src) dentro de otro más pequeño (dst):
src := []int{10, 20, 30, 40}
dst := make([]int, 2)
n := copy(dst, src)
fmt.Println(n) // 2
fmt.Println(dst) // [10 20]
Aquí:
srctiene 4 elementos: [10 20 30 40]dstse crea conmake([]int, 2)- La operación
copy(dst, src)copia elementos desdesrchaciadst.
La regla fundamental de copy() es que se copian tantos elementos como el mínimo entre len(dst) y len(src).
En este caso:
- len(src) = 4
- len(dst) = 2
- El mínimo es 2
Por eso:
- Solo se copian los dos primeros elementos.
nvale 2 (número de elementos copiados).dsttermina siendo [10 20].
Los valores 30 y 40 no se copian porque dst no tiene espacio para más elementos.
Sobrescribir un slice existente
La función copy(dst, src) copia tantos elementos como pueda sin salirse de ninguno de los dos slices.
dst := []int{1, 1, 1, 1}
src := []int{9, 8}
copy(dst, src)
fmt.Println(dst) // [9 8 1 1]
Aquí:
dsttiene 4 posicionessrctiene 2 posicionescopy(dst, src)empieza a copiar desde el índice 0 de ambos slices.
La copia se hace elemento a elemento:
dst[0] = src[0] → 9
dst[1] = src[1] → 8
En ese momento ya no hay más elementos en src. La copia se detiene. Solo se han reemplazado las dos primeras posiciones de dst. Las posiciones dst[2] y dst[3] no cambian porque src no tiene más elementos que copiar.
Consecuencias del diseño
Este modelo explica varias propiedades clave:
- El tamaño no forma parte del tipo:
[]intes el mismo tipo tenga 3 o 300 elementos. - Varios slices pueden compartir el mismo backing array.
- Modificar un slice puede afectar a otros que compartan memoria.
- Copiar un slice no copia los datos, solo copia los tres campos de la estructura.
- Puede crecer dinámicamente mediante
append(), reutilizando capacidad o realocando si es necesario.
Entender esta anatomía es fundamental para razonar correctamente sobre memoria, rendimiento y efectos colaterales en Go.
Iterar un slice
La forma habitual de recorrer un slice es usando el bucle for junto con la palabra clave range.
Ejemplo básico:
for i := range window {
fmt.Println(window[i])
}
En este patrón:
rangerecorre los índices válidos del slicewindow.ies el índice actual.- el valor actual se obtiene accediendo a
window[i].
Este enfoque es claro, seguro y no depende del tamaño del slice.
Guía rápida para usar slice o array
En la práctica, más del 90% de las colecciones en Go son slices. Los arrays son una elección consciente y poco frecuente.
En caso de duda, la siguiente tabla debería aclararte cuando usar cada uno:
| Caso de uso | Recomendación |
|---|---|
| Tamaño conocido y fijo en tiempo de compilación | Array |
| Tamaño es parte de la semántica (matriz 3×3, clave fija) | Array |
| Colecciones dinámicas, listas, colas, etc. | Slice |
| Paso de colecciones a funciones (casi siempre) | Slice |
| Código real del día a día (casi siempre) | Slice |
Resumen mental
- Los slices son la herramienta estándar para trabajar con colecciones en Go.
- Tienen len (elementos visibles) y cap (espacio disponible).
append()hace crecer el slice (y puede realocar).make()permite optimizar evitando realocaciones.copy()copia elementos desde un slice origen hacia otro destino ya existente.- Modificar un slice puede afectar otros que compartan el backing array.
cap(s) - len(s)indica cuántos elementos puedes añadir antes de que se produzca una realocación.
Conclusión
Go mantiene los arrays como bloques simples, estrictos y predecibles.
Sobre ellos construye los slices: una abstracción flexible, eficiente y segura que se adapta al flujo real de los programas sin ocultar lo que ocurre por debajo.
Entender bien los slices (y especialmente la relación entre longitud, capacidad y backing array) es uno de los pasos más importantes para escribir código Go idiomático.
Con esto dominado, el siguiente paso natural son los maps.
¡Nos vemos en el próximo artículo!
Pulso la tecla ESC:wq!
Use the share button below if you liked it.
There's not much you can do without a CPU.