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.
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ê:
sguarda três valores num array com espaço para quatro.append(s, 40)encontra uma caixa livre. Coloca 40 nela e retorna um slice com tamanho 4 sobre o mesmo array.append(s, 50)não encontra caixa livre. Aloca um array novo, maior.- 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,
lenecap. Copiar um slice copia a janela, não os valores. appendreaproveita o array enquanto há capacidade, e aloca um maior quando não há. Sempre escrevas = append(s, x).- Dois slices sobre um array podem sobrescrever um ao outro via
append. Uses[low:high:max]ouslices.Clonequando 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
nullem JSON.
Um slice não guarda seus valores. Ele diz onde encontrá-los.