Operações com slices: append, copy e crescimento
1. A Função append: Adicionando Elementos Dinamicamente
A função append é a principal ferramenta para adicionar elementos a um slice em Go. Sua sintaxe é direta:
slice = append(slice, elementos...)
O comportamento do append depende da capacidade atual do slice. Se houver espaço suficiente no array subjacente, os elementos são adicionados diretamente. Caso contrário, o Go aloca um novo array com capacidade maior, copia os elementos existentes e então adiciona os novos.
package main
import "fmt"
func main() {
// Slice com capacidade 3
frutas := make([]string, 0, 3)
frutas = append(frutas, "maçã", "banana", "laranja")
fmt.Printf("len=%d cap=%d %v\n", len(frutas), cap(frutas), frutas)
// Este append causará realocação (capacidade insuficiente)
frutas = append(frutas, "uva")
fmt.Printf("len=%d cap=%d %v\n", len(frutas), cap(frutas), frutas)
}
Para concatenar slices, usamos o operador ...:
slice1 := []int{1, 2, 3}
slice2 := []int{4, 5, 6}
combinado := append(slice1, slice2...)
fmt.Println(combinado) // [1 2 3 4 5 6]
2. O Mecanismo de Crescimento Interno do Slice
O Go utiliza uma estratégia inteligente para gerenciar o crescimento de slices. Para slices pequenos (tipicamente até 256 elementos), a capacidade dobra a cada realocação. Para slices maiores, o fator de crescimento é reduzido para evitar desperdício de memória.
package main
import "fmt"
func main() {
slice := make([]int, 0)
for i := 0; i < 10; i++ {
slice = append(slice, i)
fmt.Printf("Após adicionar %d: len=%d cap=%d\n", i, len(slice), cap(slice))
}
}
Em versões recentes do Go (1.18+), o algoritmo de crescimento é:
- Para capacidade < 256: dobra o tamanho (
cap *= 2) - Para capacidade >= 256: cresce em aproximadamente 25% (
cap += cap/4 + 192)
Isso significa que realocações frequentes têm custo O(n) devido à cópia de elementos, tornando a pré-alocação uma prática importante para slices grandes.
3. A Função copy: Copiando Dados entre Slices
A função copy permite transferir elementos entre slices de forma eficiente:
copiados := copy(destino, origem)
O número de elementos copiados é o mínimo entre len(origem) e len(destino):
package main
import "fmt"
func main() {
origem := []int{1, 2, 3, 4, 5}
destino := make([]int, 3)
copiados := copy(destino, origem)
fmt.Printf("Copiados: %d\n", copiados) // 3
fmt.Println(destino) // [1 2 3]
// Cópia parcial usando slices de expressão
destino2 := make([]int, 5)
copy(destino2[1:4], origem[2:5])
fmt.Println(destino2) // [0 3 4 5 0]
}
4. Diferenças Cruciais entre append e copy
A principal diferença está no gerenciamento de memória:
appendpode modificar o array subjacente e realocarcopynunca altera o tamanho do slice destino
package main
import "fmt"
func main() {
// Perigo: append pode causar efeitos colaterais
original := []int{1, 2, 3, 4, 5}
sub := original[:3]
// Se append não realocar, modifica o array compartilhado
sub = append(sub, 10)
fmt.Println(original) // Pode ser [1 2 3 10 5] ou [1 2 3 4 5] dependendo da realocação
// copy é seguro - não altera o slice original
copia := make([]int, 3)
copy(copia, original)
copia[0] = 99
fmt.Println(original) // [1 2 3 4 5] - inalterado
}
Use append quando precisar crescer o slice. Use copy quando precisar duplicar dados sem modificar o tamanho.
5. Gerenciamento de Capacidade: make e Pré-alocação
A função make permite criar slices com capacidade pré-definida:
slice := make([]T, comprimento, capacidade)
A pré-alocação é crucial para performance em loops:
package main
import (
"fmt"
"time"
)
func main() {
n := 1000000
// Sem pré-alocação
start := time.Now()
s1 := []int{}
for i := 0; i < n; i++ {
s1 = append(s1, i)
}
fmt.Printf("Sem pré-alocação: %v\n", time.Since(start))
// Com pré-alocação
start = time.Now()
s2 := make([]int, 0, n)
for i := 0; i < n; i++ {
s2 = append(s2, i)
}
fmt.Printf("Com pré-alocação: %v\n", time.Since(start))
}
A diferença é significativa — a pré-alocação elimina múltiplas realocações e cópias de dados.
6. Técnicas Avançadas com append e copy
Removendo elementos sem perder ordem:
func remove(slice []int, i int) []int {
return append(slice[:i], slice[i+1:]...)
}
func removePreservandoOrdem(slice []int, i int) []int {
copy(slice[i:], slice[i+1:])
return slice[:len(slice)-1]
}
Inserindo elementos no meio:
func insert(slice []int, i int, value int) []int {
slice = append(slice, 0)
copy(slice[i+1:], slice[i:])
slice[i] = value
return slice
}
Implementando uma pilha eficiente:
type Stack []int
func (s *Stack) Push(v int) {
*s = append(*s, v)
}
func (s *Stack) Pop() (int, bool) {
if len(*s) == 0 {
return 0, false
}
v := (*s)[len(*s)-1]
*s = (*s)[:len(*s)-1]
return v, true
}
7. Armadilhas Comuns e Boas Práticas
Armadilha 1: Compartilhamento acidental do array subjacente
a := []int{1, 2, 3, 4, 5}
b := a[:2]
b = append(b, 99) // Pode modificar a se não houver realocação
Solução: Sempre fazer uma cópia explícita quando precisar de independência:
b := make([]int, 2)
copy(b, a[:2])
Armadilha 2: Confundir len e cap ao usar copy
destino := make([]int, 0, 10) // len = 0
copy(destino, origem) // Não copia nada! destino tem len=0
Solução: Criar o destino com len igual ao número de elementos desejados:
destino := make([]int, len(origem))
copy(destino, origem)
Recomendações finais:
- Sempre reatribua o resultado de
append:slice = append(slice, elem) - Pré-aloque capacidade quando o tamanho final for conhecido
- Use
copypara duplicar dados de forma segura - Documente realocações em código crítico de performance
- Prefira slices de expressão para criar sub-slices sem realocar
Referências
- Effective Go: Slices — Documentação oficial sobre slices e boas práticas
- Go Slices: usage and internals — Artigo oficial do Go Blog explicando o funcionamento interno de slices
- Go Wiki: Slice Tricks — Coleção de técnicas avançadas com slices mantida pela comunidade Go
- Understanding Go's slice growth algorithm — Análise detalhada do algoritmo de crescimento de slices em diferentes versões do Go
- Go by Example: Slices — Tutoriais práticos com exemplos interativos de operações com slices
- The Go Programming Language Specification: Slice types — Especificação oficial da linguagem sobre tipos slice
- Go 1.18 Release Notes: Slice growth — Notas oficiais sobre mudanças no algoritmo de crescimento de slices no Go 1.18