Slice em Go: len, cap e armadilhas ao usar append
1. Visão geral
Slice é o tipo de dados mais usado em Go, mas também causa os bugs mais difíceis de detectar. O motivo é que múltiplos slices podem compartilhar um mesmo array subjacente. Nesse caso, append pode sobrescrever dados de outro slice sem gerar nenhum erro.
Este artigo explica len e cap, como append decide se aloca um novo array ou escreve no array existente, e técnicas para evitar erros: expressão de slice completa, copy, slices.Clone. Depois de ler, você conseguirá prever o resultado correto de código com append e saberá como escrever testes para detectar erros de aliasing.
Os exemplos usam Go 1.21 ou superior, pois o package slices foi introduzido nesta versão.
2. O que um slice realmente é
Um slice é um struct pequeno composto por três campos:
- pointer: aponta para o primeiro elemento no array subjacente (backing array).
- len: número de elementos aos quais você pode acessar.
- cap: número de elementos do pointer até o final do array subjacente.
Quando você passa um slice para uma função ou atribui a outra variável, Go apenas copia este struct de três campos. O array subjacente não é copiado.
2.1. Observando len e cap
package main
import "fmt"
func main() {
orders := []string{"A01", "A02", "A03", "A04", "A05"}
recent := orders[1:3]
fmt.Println(recent, len(recent), cap(recent))
}
[A02 A03] 2 4
orders[1:3] obtém os elementos do índice 1 até antes do índice 3, então len é 2. cap é 4 porque da posição orders[1] até o final do array há 4 posições: A02, A03, A04, A05.
2.2. Dois slices, um array
Como recent aponta para o mesmo array que orders, modificar um elemento neste slice afetará imediatamente o outro slice:
recent[0] = "CANCELLED"
fmt.Println(orders)
[A01 CANCELLED A03 A04 A05]
Este é um comportamento intencional. Ajuda a tornar o slicing economicamente viável. Mas também é a raiz de todas as armadilhas nas seções seguintes.
3. Como append funciona
append(s, x) faz uma de duas coisas:
- Se
len(s) < cap(s): escrevexna posiçãos[len(s)]no array atual, depois retorna um novo slice comlen + 1. - Se
len(s) == cap(s): aloca um novo array maior, copia os dados antigos para ele, escrevex, depois retorna um slice apontando para o novo array.
É por isso que o caso 2 pode acontecer, você sempre deve reatribuir o resultado: s = append(s, x).
3.1. Observando cap crescer gradualmente
package main
import "fmt"
func main() {
var ids []int
for i := 0; i < 10; i++ {
ids = append(ids, i)
fmt.Printf("len=%d cap=%d\n", len(ids), cap(ids))
}
}
len=1 cap=1
len=2 cap=2
len=3 cap=4
len=4 cap=4
len=5 cap=8
len=6 cap=8
len=7 cap=8
len=8 cap=8
len=9 cap=16
len=10 cap=16
Para slices pequenos, cap normalmente dobra cada vez que fica cheio. Para slices maiores, o runtime cresce mais lentamente, e o resultado é arredondado de acordo com a classe de tamanho do alocador.
Nota: A fórmula para aumentar
capé um detalhe de implementação do runtime e mudou ao longo das versões do Go. Não escreva código que dependa de um valorcapespecífico apósappend.
4. Armadilha 1: append sobrescreve dados do slice original
Este é o erro mais comum:
package main
import "fmt"
func main() {
prices := []int{10, 20, 30, 40, 50}
firstTwo := prices[:2]
firstTwo = append(firstTwo, 99)
fmt.Println("firstTwo:", firstTwo)
fmt.Println("prices: ", prices)
}
firstTwo: [10 20 99]
prices: [10 20 99 40 50]
firstTwo tem len = 2 mas cap = 5. Como o array ainda tem espaço, append não aloca um novo array, mas escreve 99 na terceira posição do array compartilhado. O resultado é que prices[2] muda de 30 para 99.
Este bug é muito difícil de encontrar. O código não entra em pânico, e o valor sobrescrito pode se revelar apenas em um módulo diferente, muito tempo depois.
5. Armadilha 2: dois appends do mesmo slice
package main
import "fmt"
func main() {
base := make([]string, 3, 4)
base[0], base[1], base[2] = "pen", "book", "bag"
cartA := append(base, "laptop")
cartB := append(base, "phone")
fmt.Println("cartA:", cartA)
fmt.Println("cartB:", cartB)
}
cartA: [pen book bag phone]
cartB: [pen book bag phone]
base ainda tem exatamente um slot vazio. O primeiro append escreve "laptop" nele. O segundo escreve "phone" no mesmo slot. Ambos cartA e cartB apontam para o mesmo array, então "laptop" desapareceu.
Se base tivesse cap = 3, ambos os append alocariam arrays separados e o resultado seria o esperado. Com o mesmo código, o comportamento muda com cap. É por isso que esse bug só aparece ocasionalmente.
6. Armadilha 3: append dentro de uma função não muda o slice do chamador
package main
import "fmt"
func addItem(items []string, item string) {
items = append(items, item)
}
func main() {
cart := make([]string, 0, 10)
addItem(cart, "pen")
fmt.Println(len(cart), cart)
fmt.Println(cart[:1])
}
0 []
[pen]
A função addItem recebe uma cópia do struct slice. append escreveu "pen" no array compartilhado, porque ainda há cap. Mas o novo len só foi atribuído à variável local items. A variável cart em main ainda tem len = 0. Os dados estão no array, mas você só os vê quando deliberadamente faz slice cart[:1].
A correção é retornar o novo slice:
func addItem(items []string, item string) []string {
return append(items, item)
}
// uso
cart = addItem(cart, "pen")
Esta é também a razão pela qual a biblioteca padrão projeta append como uma função que retorna um valor, em vez de modificar o slice no lugar.
7. Armadilha 4: confundir make com len e make com cap
package main
import "fmt"
func main() {
names := []string{"An", "Binh", "Chi"}
upper := make([]string, len(names)) // intenção errada
for _, n := range names {
upper = append(upper, n+"!")
}
fmt.Println(len(upper), upper)
}
6 [ An! Binh! Chi!]
make([]string, 3) cria um slice com 3 elementos vazios, não um slice vazio com capacidade 3. append adiciona após esses 3 strings vazios.
Você tem duas maneiras corretas de corrigir. Escolha uma, não misture:
// Forma 1: len = 0, cap = n, depois append
upper := make([]string, 0, len(names))
for _, n := range names {
upper = append(upper, n+"!")
}
// Forma 2: len = n, atribuir por índice
upper := make([]string, len(names))
for i, n := range names {
upper[i] = n + "!"
}
Ambas alocam uma única vez, mais rápido do que deixar append aumentar cap várias vezes.
8. Formas de prevenir
8.1. Expressão de slice completa: limitando cap
A sintaxe s[low:high:max] define cap = max - low. Quando cap é igual a len, o próximo append força a alocação de um novo array:
prices := []int{10, 20, 30, 40, 50}
firstTwo := prices[:2:2] // len=2, cap=2
firstTwo = append(firstTwo, 99)
fmt.Println("firstTwo:", firstTwo)
fmt.Println("prices: ", prices)
firstTwo: [10 20 99]
prices: [10 20 30 40 50]
prices não é afetado. Essa técnica é útil quando você retorna uma parte de um slice interno e quer que o chamador possa fazer append sem corromper seus dados.
8.2. slices.Clone e copy: separando completamente o array
Quando você precisa de uma cópia completamente independente, clone:
import "slices"
orders := []string{"A01", "A02", "A03"}
snapshot := slices.Clone(orders)
snapshot[0] = "X"
fmt.Println(orders[0], snapshot[0]) // A01 X
Para Go anterior a 1.21, use copy:
snapshot := make([]string, len(orders))
copy(snapshot, orders)
copy retorna o número de elementos copiados, igual ao menor entre len(dst) e len(src). Portanto, dst deve ter len suficientemente grande, não apenas cap. Um erro comum é escrever copy(make([]string, 0, n), src) e receber um slice vazio.
8.3. A armadilha de reter memória
Fazer slice de um pedaço pequeno de um array grande manterá todo o array grande na memória:
func firstLine(data []byte) []byte {
for i, b := range data {
if b == '\n' {
return slices.Clone(data[:i]) // não retém todo data
}
}
return slices.Clone(data)
}
Se data é um arquivo de 100 MB e você retorna data[:i] sem clonar, o garbage collector não poderá liberar esses 100 MB enquanto o slice resultante for referenciado.
9. Escrevendo testes para detectar erros de aliasing
Suponha que você tenha um tipo Inventory que retorna uma lista de itens. Você quer ter certeza de que o chamador fazer append ao resultado não corrompa os dados internos:
// inventory.go
package inventory
type Inventory struct {
items []string
}
func New(items ...string) *Inventory {
return &Inventory{items: items}
}
func (inv *Inventory) Items() []string {
return inv.items[:len(inv.items):len(inv.items)]
}
// inventory_test.go
package inventory
import (
"slices"
"testing"
)
func TestItems_AppendDoesNotCorruptInternalState(t *testing.T) {
inv := New("pen", "book")
inv.items = append(inv.items[:0:0], "pen", "book", "bag")[:2] // cap com espaço extra intencionalmente
got := inv.Items()
got = append(got, "laptop")
want := []string{"pen", "book"}
if !slices.Equal(inv.Items(), want) {
t.Fatalf("itens internos = %v, esperado %v", inv.Items(), want)
}
if inv.items[:3][2] != "bag" {
t.Fatalf("backing array foi sobrescrito: %v", inv.items[:3])
}
if !slices.Equal(got, []string{"pen", "book", "laptop"}) {
t.Fatalf("got = %v", got)
}
}
O teste deliberadamente cria um slice interno com cap maior que len, porque essa é a condição para o bug aparecer. Se você mudar Items() para return inv.items, o comando append(got, "laptop") sobrescreveria "bag" e o teste falharia na segunda asserção.
go test ./...
ok example.com/inventory 0.002s
Nota: Testes de aliasing sempre devem construir a situação
len < cap. Se o slice no teste for criado com um literal como[]string{"pen", "book"}, entãolen == cap,appendsempre aloca um novo array, e o teste passará mesmo que o código tenha um bug.
10. Quando NÃO se preocupar
Nem todo slice precisa ser clonado. Cópia desperdiça memória e CPU. Você pode compartilhar um array com segurança quando:
- O slice é apenas leitura, ninguém faz
appendou modifica elementos. - O slice é uma variável local, não é compartilhado fora da função.
- Você intencionalmente quer modificar no local, por exemplo, filtrar elementos usando
s[:0]para reusar o array:
active := orders[:0]
for _, o := range orders {
if o != "CANCELLED" {
active = append(active, o)
}
}
Essa técnica é segura porque active nunca sobrescreve além da posição que está sendo lida de orders. Após o loop, você deve usar apenas active e descartar orders.
11. Conclusão
Um slice é um struct composto de pointer, len e cap que aponta para um array que pode ser compartilhado. append aloca um novo array apenas quando len == cap. Em todos os outros casos, escreve diretamente no array compartilhado. Esta é a origem das quatro armadilhas que encontramos: sobrescrever o slice original, dois append se sobrepondo, append em uma função não afetando o chamador, e confundir len com cap no make.
Para ficar seguro, lembre-se de algumas regras:
- Sempre escreva
s = append(s, ...). - Funções que adicionam elementos devem retornar um novo slice.
- Use
s[low:high:high]quando retornar um slice interno para fora. - Use
slices.Clonequando precisar de uma cópia independente ou quiser liberar um array grande.
Ao escrever testes, lembre-se de construir a condição len < cap para que o bug tenha oportunidade de aparecer.