Blog

Arrays e slices em Go: len, cap e como o append funciona

Um slice em Go é uma pequena janela sobre um array que guarda os valores de verdade. Quando você enxerga a janela, len, cap, append e o bug em que dois slices mudam um ao outro deixam de ser mistério.

Slices são o tipo de Go que você mais vai usar. São também o que mais surpreende, porque um slice parece uma lista e quase sempre age como uma. Mas não é uma lista.

Este post começa pelos arrays, porque um slice é construído em cima de um. Todo programa abaixo rodou no Go 1.26, e a saída foi colada da execução.

Arrays: tamanho fixo, e copiados inteiros

Um array tem um tamanho que faz parte do tipo dele. [3]int e [4]int são tipos diferentes, e nenhum dos dois pode crescer.

package main

import "fmt"

func main() {
	a := [3]int{1, 2, 3}
	b := a // copies all three values
	b[0] = 99
	fmt.Println(a, b)
}

Ele imprime:

[1 2 3] [99 2 3]

Atribuir um array copia cada elemento. b é um conjunto novo de três caixas, então mudá-lo não mexe em a.

Isso deixa os arrays previsíveis. Também faz com que sejam raros no código do dia a dia, porque você quase nunca sabe o tamanho exato de antemão. O que você usa no lugar é um slice.

Um slice é uma janela sobre um array

package main

import "fmt"

func main() {
	shelf := [6]string{"a", "b", "c", "d", "e", "f"}
	s := shelf[1:4]
	fmt.Println(s, len(s), cap(s))

	s[0] = "B"
	fmt.Println(shelf)
}

Ele imprime:

[b c d] 3 5
[a B c d e f]

shelf[1:4] não copia nada. Cria um slice que olha para os elementos 1, 2 e 3 do array que já existe. Escrever s[0] = "B" mudou shelf, porque só existe um conjunto de caixas.

Explicado como se você tivesse dez anos

Imagine uma prateleira comprida de caixas numeradas. Essa é o array. É onde as coisas moram de verdade.

Um slice é uma janela de papelão que você segura na frente da prateleira. A janela tem três coisas escritas nela:

  • onde ela começa na prateleira
  • quantas caixas ela mostra. Isso é o len.
  • quantas caixas existem do começo dela até o fim da prateleira. Isso é o cap.

Se você enfia a mão pela janela e troca um brinquedo, o brinquedo da prateleira muda. Qualquer outra pessoa segurando uma janela sobre as mesmas caixas também vê o brinquedo novo.

A versão precisa

Um valor de slice são três palavras: um ponteiro para um elemento de um array subjacente, um tamanho e uma capacidade. Copiar um slice, ou passá-lo para uma função, copia essas três palavras. Nunca copia os elementos.

len(s) é quantos elementos você pode indexar. cap(s) é quantos elementos existem do começo do slice até o fim do array subjacente. No exemplo, s começa no elemento 1 de um array de 6 elementos, então a capacidade dele é 5.

Onde a analogia falha: uma janela de verdade não consegue deixar a prateleira mais comprida. append consegue fazer algo parecido, e a próxima seção mostra como.

append: a mesma prateleira se houver espaço, uma nova se não houver

append adiciona valores a um slice, e se ele reaproveita o array existente ou aloca um novo decide se o seu programa tem um bug. Veja primeiro, depois leia as regras.

array A 10 20 30 40 array antigo: ninguém aponta aqui array B 1020 3040 50 s: len 3, cap 4, no array A s: len 4, cap 4, no array A s: len 5, cap 8, no array B s := []int{10, 20, 30}, com espaço para 4 s = append(s, 40): há espaço, então usa o mesmo array s = append(s, 50): sem espaço, Go cria um array maior os valores são copiados, 50 entra, e s passa para o B

append num slice com tamanho 3 e capacidade 4. O primeiro append cabe na caixa livre, então o mesmo array é usado. O segundo não encontra espaço: Go aloca um array maior, copia os valores, adiciona 50, e o slice passa a apontar para o array novo. Nada mais aponta para o array antigo, então o garbage collector vai recuperá-lo.

Aqui estão esses passos em palavras, caso a animação não rode para você:

  1. s guarda três valores num array com espaço para quatro.
  2. append(s, 40) encontra uma caixa livre. Coloca 40 nela e retorna um slice com tamanho 4 sobre o mesmo array.
  3. append(s, 50) não encontra caixa livre. Aloca um array novo, maior.
  4. Copia os quatro valores, adiciona 50 e retorna um slice sobre o array novo. O array antigo fica para trás.

É por isso que você sempre escreve s = append(s, x). append pode devolver um slice sobre outro array, e se você não guardar o resultado, fica com a janela antiga.

Quanto maior?

A especificação do Go não diz. É um detalhe do runtime, então é melhor observar do que decorar:

package main

import "fmt"

func main() {
	var s []int
	prev := -1
	for i := range 2000 {
		s = append(s, i)
		if cap(s) != prev {
			fmt.Printf("len %4d  cap %4d\n", len(s), cap(s))
			prev = cap(s)
		}
	}
}

Ele imprime:

len    1  cap    4
len    5  cap    8
len    9  cap   16
len   17  cap   32
len   33  cap   64
len   65  cap  128
len  129  cap  256
len  257  cap  512
len  513  cap  848
len  849  cap 1280
len 1281  cap 1792
len 1793  cap 2560

Duas coisas merecem atenção. O primeiro append já reserva espaço para quatro, e depois os slices pequenos dobram. Passando de algumas centenas de elementos, o crescimento desacelera, para um slice enorme não desperdiçar memória enorme. Os números exatos podem mudar entre versões do Go. O que não muda é a regra: um array novo só é alocado quando o antigo está cheio.

Se você sabe mais ou menos quantos elementos vêm por aí, diga isso de antemão e evite o crescimento repetido:

package main

import "fmt"

func main() {
	ids := make([]int, 0, 1000)
	for i := range 1000 {
		ids = append(ids, i)
	}
	fmt.Println(len(ids), cap(ids))
}

Ele imprime:

1000 1000

make([]int, 0, 1000) dá tamanho 0 e capacidade 1000. Os mil appends cabem, e nada é copiado.

O bug: dois slices que mudam um ao outro

Junte o array compartilhado com a regra “a mesma prateleira se houver espaço” e você tem o bug de slice mais comum em Go:

package main

import "fmt"

func main() {
	base := make([]int, 3, 4)
	a := append(base, 1)
	b := append(base, 2)
	fmt.Println(a, b)
}

Ele imprime:

[0 0 0 2] [0 0 0 2]

Você provavelmente esperava [0 0 0 1] [0 0 0 2]. Veja o que aconteceu. base tem uma caixa livre. O primeiro append escreveu 1 nela. O segundo append também tinha espaço, então escreveu 2 na mesma caixa. a e b são duas janelas sobre um array, e a última escrita vence.

Nada quebrou, e nada avisou você. É por isso que esse bug passa pelo code review.

A correção: limite a capacidade

Uma expressão de slice completa, s[low:high:max], define a capacidade também. Sem espaço livre, append é obrigado a alocar:

package main

import "fmt"

func main() {
	base := make([]int, 3, 4)
	a := append(base[:3:3], 1)
	b := append(base[:3:3], 2)
	fmt.Println(a, b)
}

Ele imprime:

[0 0 0 1] [0 0 0 2]

base[:3:3] quer dizer “os três primeiros elementos, com capacidade 3”. Agora cada append encontra a janela cheia, então cada um ganha um array próprio.

Quando você só quer uma cópia independente, slices.Clone diz isso de forma mais direta. Ele aparece de novo mais abaixo.

Passando slices para funções

Passar um slice para uma função em Go copia o cabeçalho de três palavras do slice, e essa cópia aponta para o mesmo array:

package main

import "fmt"

func setFirst(s []int) {
	s[0] = 100
}

func addOne(s []int) {
	s = append(s, 1)
}

func main() {
	nums := []int{1, 2, 3}
	setFirst(nums)
	addOne(nums)
	fmt.Println(nums)
}

Ele imprime:

[100 2 3]

setFirst mudou o array compartilhado, então quem chamou vê 100. addOne fez append, mas só na própria cópia do cabeçalho. O nums de quem chamou continua com tamanho 3, então o elemento extra nunca aparece.

Uma função que precisa aumentar um slice retorna o novo, do mesmo jeito que append faz: nums = addOne(nums).

Slices nil e vazios

Um slice nil e um slice vazio têm tamanho 0, mas não são o mesmo valor:

package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	var nilSlice []int
	empty := []int{}
	fmt.Println(nilSlice == nil, empty == nil)
	fmt.Println(len(nilSlice), len(empty))

	a, _ := json.Marshal(nilSlice)
	b, _ := json.Marshal(empty)
	fmt.Println(string(a), string(b))
}

Ele imprime:

true false
0 0
null []

Os dois têm tamanho 0. Você pode usar range nos dois e fazer append nos dois. Um slice nil é um slice vazio perfeitamente bom, então var s []int é o jeito normal de começar um.

A diferença aparece nas bordas. Um slice nil vira null em JSON, e um vazio vira []. Isso importa quando você construir uma API REST mais adiante nesta série. Um cliente que espera um array pode quebrar com null.

Passando do fim

Ler um slice além do tamanho dele é um panic em tempo de execução em Go, não uma leitura silenciosa da memória que vier depois:

package main

import "fmt"

func main() {
	s := []int{1, 2, 3}
	fmt.Println(s[:cap(s)])
	fmt.Println(s[3])
}

Imprime a primeira linha e para:

[1 2 3]
panic: runtime error: index out of range [3] with length 3

A indexação é conferida contra o len, e o fatiamento contra o cap. Go nunca lê memória além do fim sem avisar. Ele para o programa com um panic, que você vai conhecer direito na parte sobre erros.

O pacote slices

Desde o Go 1.21, a biblioteca padrão tem um pacote slices para as tarefas que antes pediam um laço escrito na mão:

package main

import (
	"fmt"
	"slices"
)

func main() {
	scores := []int{42, 7, 19, 7}
	clone := slices.Clone(scores)
	slices.Sort(clone)

	fmt.Println(scores, clone)
	fmt.Println(slices.Contains(scores, 19), slices.Index(scores, 7))
	fmt.Println(slices.Compact(clone))
}

Ele imprime:

[42 7 19 7] [7 7 19 42]
true 1
[7 19 42]

slices.Clone cria um slice sobre um array novo, então ordenar o clone não mexeu em scores. slices.Compact remove duplicatas consecutivas, e é por isso que ele é usado depois do Sort.

O que lembrar

  • Um array tem um tamanho fixo que faz parte do tipo, e atribuir um array copia cada elemento.
  • Um slice é uma janela sobre um array: um ponteiro, len e cap. Copiar um slice copia a janela, não os valores.
  • append reaproveita o array enquanto há capacidade, e aloca um maior quando não há. Sempre escreva s = append(s, x).
  • Dois slices sobre um array podem sobrescrever um ao outro via append. Use s[low:high:max] ou slices.Clone quando eles precisarem ser independentes.
  • Uma função pode mudar os elementos de um slice, mas não o tamanho dele. Retorne o slice novo.
  • Um slice nil é um bom slice vazio, mas vira null em JSON.

Um slice não guarda seus valores. Ele diz onde encontrá-los.

Quanto este post te ajudou?

Clique em um coração para avaliar!

Média das avaliações 0 / 5. Total de votos: 0

Nenhum voto até agora. Seja o primeiro a avaliar este post.