Menu

Slices en Golang: append, make, len y cap, y el bug de compartir

Los slices son el tipo lista de uso diario en Go. Aprende make, append y el crecimiento, len frente a cap, cómo recortar comparte el array subyacente (y el bug que provoca), copy, el paquete slices, slices 2D y nil frente a vacío.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Un slice en un ejemplo

Un slice es una vista ampliable sobre un array. Se crea con un literal o con make, se hace crecer con append y su tamaño se lee con len.

[]string no lleva longitud entre los corchetes. Esa es la diferencia con un tipo array como [4]string, cuyo tamaño es fijo.

Qué es un slice

Un valor slice es una cabecera pequeña con tres campos:

  • un puntero a un elemento de un array subyacente
  • una longitud: cuántos elementos puede ver el slice
  • una capacidad: cuántos elementos hay desde ese puntero hasta el final del array subyacente

Copiar un slice (asignarlo, pasarlo a una función) copia solo esta cabecera. Las dos copias apuntan a los mismos elementos. Casi todas las sorpresas con slices vienen de olvidar esto.

make: longitud y capacidad

make([]T, len, cap) reserva un array subyacente y devuelve un slice sobre él. La capacidad es opcional y por defecto es igual a la longitud.

Una confusión habitual: make([]int, 5) seguido de cinco llamadas a append te da diez elementos, los cinco primeros a cero. Usa make([]int, 0, 5) cuando vayas a hacer append, o make([]int, 5) cuando vayas a asignar por índice.

Reservar capacidad de antemano cuando conoces el tamaño final evita crecimientos repetidos. Es una optimización, no un requisito.

append y el crecimiento

append añade elementos detrás de la longitud actual. Si la capacidad basta, escribe en el sitio. Si no, reserva un array más grande, copia los elementos antiguos y devuelve un slice que apunta al array nuevo.

La capacidad aproximadamente se duplica mientras el slice es pequeño y crece en un factor menor (tendiendo a 1,25x) una vez que pasa de 256 elementos. Los números exactos son un detalle del runtime que ha cambiado entre versiones de Go, así que nunca escribas código que dependa de ellos.

Como append puede devolver un array distinto, tienes que usar su resultado:

append(s, 4)     // compile error: append(s, 4) (value of type []int) is not used
s = append(s, 4) // correct

Añadir un slice entero usa la sintaxis de expansión: s = append(s, other...).

Recortar comparte el array subyacente

s[low:high] crea una nueva cabecera de slice sobre el mismo array, que cubre los índices desde low hasta high sin incluirlo. Se puede omitir cualquiera de los dos límites. No se copia ningún elemento.

Salida:

[10 99 30 40 50]
3 4

Esa compartición es lo que hace barato recortar. También es el origen del bug de slices más famoso.

El bug de aliasing con append

Cuando un sub-slice tiene capacidad sobrante, hacer append sobre él escribe en el array del padre, encima de elementos que el padre todavía usa.

Salida:

[1 2 100 4 5]
[1 2 100]
[cmd test] [cmd test]

a y b comparten la posición del array que va detrás de "cmd", así que el segundo append sobrescribió el primero. El código parece correcto y funciona siempre que la capacidad esté justo llena, y por eso este bug solo aparece a veces.

Dos arreglos:

  1. La expresión de slice completa s[low:high:max] limita la capacidad a max-low. Sin capacidad sobrante, el siguiente append tiene que reservar memoria. first := base[:2:2] hace seguro el primer ejemplo.
  2. Copia de forma explícita cuando un slice vaya a sobrevivir a la llamada o a recibir appends por su cuenta: slices.Clone(prefix) o append([]string(nil), prefix...).

Una trampa relacionada: un slice pequeño de un array enorme mantiene vivo el array entero para el recolector de basura. Si lees un archivo de 100 MB y te quedas con data[:10], los 100 MB se quedan en memoria. Clona la parte que conservas.

copy

La función integrada copy(dst, src) copia min(len(dst), len(src)) elementos y devuelve ese número. Nunca hace crecer dst.

copy trata correctamente el solapamiento entre origen y destino, así que copy(s[1:], s) desplaza los elementos a la derecha sin corromperlos.

El paquete slices

Desde Go 1.21, el paquete estándar slices tiene funciones genéricas para las operaciones que antes escribías a mano.

FunciónQué hace
Contains, Indexbuscar un valor (ContainsFunc, IndexFunc reciben un predicado)
Sort, SortFunc, SortStableFuncordenar en el sitio
BinarySearchbuscar en un slice ordenado
Insert, Delete, DeleteFuncinsertar o quitar, devuelven el slice nuevo
Compactquitar los duplicados consecutivos
Equal, Comparecomparar elemento a elemento
Clone, Reverse, Max, Minlo que su nombre indica
Collect, Sorted, Values, Alltrabajar con iteradores (Go 1.23)

Delete e Insert devuelven un slice que tienes que asignar, igual que append. Ordenar tiene su propia página: ordenación.

Quitar elementos mientras recorres por índice se salta elementos. Usa slices.DeleteFunc:

s = slices.DeleteFunc(s, func(n int) bool { return n%2 == 0 }) // drop evens

Slices 2D

Un slice de slices te da una cuadrícula cuyas filas pueden tener longitudes distintas. Cada fila tiene que reservarse por separado.

Olvidar el make interior deja cada fila a nil, y grid[1][2] = 7 provoca un panic con index out of range [2] with length 0.

Slices nil y slices vacíos

Un slice nil se comporta como uno vacío con len, cap, range y append, así que prefiere var s []T como estado cero. Inicializa a []T{} solo cuando la diferencia importe, que es sobre todo la salida JSON (null frente a []). Comprueba si está vacío con len(s) == 0, no con s == nil.

Slices y funciones

Una función que recibe un slice puede cambiar sus elementos, y quien la llama ve los cambios. No puede cambiar la longitud del slice de quien llama, porque recibió una copia de la cabecera. Una función que hace append tiene que devolver el slice nuevo:

func addAll(s []int, vals ...int) []int {
	return append(s, vals...)
}

Por eso append, slices.Delete y slices.Insert devuelven todos un slice.

Errores comunes

  • Ignorar el resultado de append. Descartarlo del todo no compila, pero asignar append(s, x) a una variable distinta de la que sigues usando es un bug de lógica.
  • Dos appends desde la misma base. Pueden compartir almacenamiento. Clona la base o usa una expresión de slice completa.
  • make([]T, n) y luego append. Deja n ceros al principio.
  • Índice fuera de rango. s[len(s)] provoca un panic. El último elemento es s[len(s)-1].
  • Comparar slices con ==. Solo compila s == nil. Usa slices.Equal.

Preguntas frecuentes

¿Qué diferencia hay entre la longitud y la capacidad de un slice en Go?

len(s) es cuántos elementos contiene ahora el slice. cap(s) es cuántos elementos caben en el array subyacente a partir del primer elemento del slice. append escribe en la capacidad sobrante cuando la hay, y reserva un array nuevo y más grande cuando no.

¿Cómo se añade a un slice en Go?

Llama a la función integrada append y vuelve a asignar el resultado: s = append(s, x). Puede añadir varios valores (append(s, 1, 2, 3)) u otro slice (append(s, other...)). Usa siempre el valor de retorno, porque append puede devolver un slice que apunta a un array nuevo.

¿Cómo elimino un elemento de un slice en Go?

Usa slices.Delete(s, i, i+1) (Go 1.21), que desplaza los elementos posteriores y devuelve el slice más corto. Desde Go 1.22 también pone a cero las posiciones liberadas del final. Si el orden no importa, intercambia con el último elemento y trunca: s[i] = s[len(s)-1]; s = s[:len(s)-1].

¿Cómo compruebo si un slice contiene un valor en Go?

Usa slices.Contains(s, v) del paquete estándar slices (Go 1.21). slices.Index(s, v) devuelve la posición o -1, y slices.ContainsFunc recibe un predicado. Para búsquedas repetidas sobre muchos datos, construye un map.

¿Qué diferencia hay entre un slice nil y un slice vacío en Go?

var s []int es nil; s := []int{} está vacío pero no es nil. Los dos tienen longitud 0 y los dos funcionan con len, range y append. La diferencia aparece en las comprobaciones s == nil y en encoding/json, que codifica un slice nil como null y uno vacío como [].

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR