Menu

Slices em Golang: append, make, len e cap e o bug do compartilhamento

Slices são o tipo de lista do dia a dia em Go. Veja make, append e o crescimento, len e cap, como o fatiamento compartilha o array subjacente (e o bug que isso causa), copy, o pacote slices, slices 2D e nil ou vazio.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

Um slice em um exemplo

Um slice é uma visão expansível sobre um array. Você cria um com um literal ou com make, aumenta com append e lê o tamanho com len.

[]string não tem tamanho entre os colchetes. Essa é a diferença para um tipo array como [4]string, cujo tamanho é fixo.

O que é um slice

Um valor de slice é um cabeçalho pequeno com três campos:

  • um ponteiro para um elemento de um array subjacente
  • um tamanho (length): quantos elementos o slice enxerga
  • uma capacidade (capacity): quantos elementos existem desse ponteiro até o fim do array subjacente

Copiar um slice (atribuí-lo, passá-lo para uma função) copia só esse cabeçalho. As duas cópias apontam para os mesmos elementos. Quase toda surpresa com slices vem de esquecer isso.

make: tamanho e capacidade

make([]T, len, cap) aloca um array subjacente e devolve um slice sobre ele. A capacidade é opcional e, por padrão, é igual ao tamanho.

Uma confusão comum: make([]int, 5) seguido de cinco chamadas de append dá dez elementos, sendo os cinco primeiros zero. Use make([]int, 0, 5) quando for fazer append, ou make([]int, 5) quando for atribuir por índice.

Pré-alocar a capacidade quando você sabe o tamanho final evita crescimentos repetidos. É uma otimização, não uma exigência.

append e crescimento

O append acrescenta elementos depois do tamanho atual. Se a capacidade for suficiente, ele escreve no lugar. Se não for, aloca um array maior, copia os elementos antigos e devolve um slice que aponta para o array novo.

A capacidade mais ou menos dobra enquanto o slice é pequeno e cresce por um fator menor (tendendo a 1,25x) depois de passar de 256 elementos. Os números exatos são um detalhe do runtime que já mudou entre versões do Go, então nunca escreva código que dependa deles.

Como o append pode devolver outro array, você precisa usar o resultado dele:

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

Acrescentar um slice inteiro usa a sintaxe de espalhamento: s = append(s, other...).

Fatiar compartilha o array subjacente

s[low:high] cria um novo cabeçalho de slice sobre o mesmo array, cobrindo os índices de low até high, sem incluir high. Qualquer um dos limites pode ser omitido. Nenhum elemento é copiado.

Saída:

[10 99 30 40 50]
3 4

Esse compartilhamento é o que torna o fatiamento barato. É também a origem do bug de slice mais famoso.

O bug de aliasing do append

Quando um sub-slice tem capacidade sobrando, fazer append nele escreve no array do slice pai, por cima de elementos que o pai ainda usa.

Saída:

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

a e b compartilham a posição do array logo depois de "cmd", então o segundo append sobrescreveu o primeiro. O código parece correto e funciona sempre que a capacidade por acaso está cheia, e é por isso que esse bug só aparece às vezes.

Duas correções:

  1. A expressão de slice completa s[low:high:max] limita a capacidade a max-low. Sem capacidade sobrando, o próximo append precisa alocar. first := base[:2:2] deixa o primeiro exemplo seguro.
  2. Copie explicitamente quando um slice for sobreviver à chamada ou receber appends de forma independente: slices.Clone(prefix) ou append([]string(nil), prefix...).

Uma armadilha parecida: um slice pequeno de um array enorme mantém o array inteiro vivo para o coletor de lixo. Se você lê um arquivo de 100 MB e guarda data[:10], os 100 MB continuam na memória. Clone a parte que vai guardar.

copy

A função embutida copy(dst, src) copia min(len(dst), len(src)) elementos e devolve essa contagem. Ela nunca aumenta dst.

O copy lida corretamente com origem e destino sobrepostos, então copy(s[1:], s) desloca os elementos para a direita sem corromper nada.

O pacote slices

Desde o Go 1.21, o pacote padrão slices tem funções genéricas para as operações que você antes escrevia à mão.

FunçãoO que faz
Contains, Indexencontra um valor (ContainsFunc e IndexFunc recebem um predicado)
Sort, SortFunc, SortStableFuncordena no lugar
BinarySearchbusca em um slice ordenado
Insert, Delete, DeleteFuncinsere ou remove e devolve o slice novo
Compactremove duplicados consecutivos
Equal, Comparecompara elemento por elemento
Clone, Reverse, Max, Mino que o nome diz
Collect, Sorted, Values, Alltrabalham com iteradores (Go 1.23)

Delete e Insert devolvem um slice que você precisa atribuir, como o append. Ordenação tem página própria: ordenação.

Remover elementos durante um laço por índice pula elementos. Use slices.DeleteFunc no lugar:

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

Slices 2D

Um slice de slices dá uma grade cujas linhas podem ter tamanhos diferentes. Cada linha precisa ser alocada separadamente.

Esquecer o make interno deixa cada linha nil, e grid[1][2] = 7 causa panic com index out of range [2] with length 0.

Slices nil e slices vazios

Um slice nil se comporta como um vazio para len, cap, range e append, então prefira var s []T como estado inicial. Inicialize com []T{} só quando a diferença importar, o que acontece principalmente na saída JSON (null ou []). Verifique se está vazio com len(s) == 0, não com s == nil.

Slices e funções

Uma função que recebe um slice pode mudar os elementos dele, e quem chama vê as mudanças. Ela não pode mudar o tamanho do slice de quem chama, porque recebeu uma cópia do cabeçalho. Uma função que faz append precisa devolver o slice novo:

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

É por isso que append, slices.Delete e slices.Insert devolvem um slice.

Erros comuns

  • Ignorar o resultado do append. Descartá-lo por completo não compila, mas atribuir append(s, x) a uma variável diferente da que você continua usando é um bug de lógica.
  • Dois appends a partir da mesma base. Eles podem compartilhar o armazenamento. Clone a base ou use uma expressão de slice completa.
  • make([]T, n) seguido de append. Deixa n zeros no início.
  • Índice fora da faixa. s[len(s)] causa panic. O último elemento é s[len(s)-1].
  • Comparar slices com ==.s == nil compila. Use slices.Equal.

Perguntas frequentes

Qual a diferença entre tamanho e capacidade de um slice em Go?

len(s) é quantos elementos o slice guarda no momento. cap(s) é quantos elementos cabem no array subjacente a partir do primeiro elemento do slice. O append escreve na capacidade sobrando quando existe alguma, e aloca um array novo e maior quando não existe.

Como fazer append em um slice em Go?

Chame a função embutida append e atribua o resultado de volta: s = append(s, x). Ela pode acrescentar vários valores (append(s, 1, 2, 3)) ou outro slice (append(s, other...)). Sempre use o valor de retorno, porque o append pode devolver um slice que aponta para um array novo.

Como remover um elemento de um slice em Go?

Use slices.Delete(s, i, i+1) (Go 1.21), que desloca os elementos seguintes para trás e devolve o slice mais curto. Desde o Go 1.22 ele também zera as posições liberadas no final. Se a ordem não importar, troque com o último elemento e corte: s[i] = s[len(s)-1]; s = s[:len(s)-1].

Como verificar se um slice contém um valor em Go?

Use slices.Contains(s, v) do pacote padrão slices (Go 1.21). slices.Index(s, v) devolve a posição ou -1, e slices.ContainsFunc recebe um predicado. Para buscas repetidas em dados grandes, monte um map.

Qual a diferença entre um slice nil e um slice vazio em Go?

var s []int é nil; s := []int{} é vazio, mas não é nil. Os dois têm tamanho 0 e funcionam com len, range e append. A diferença aparece em verificações s == nil e no encoding/json, que codifica um slice nil como null e um slice vazio como [].

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR