Slices: o tipo de coleção fundamental do Go
1. Introdução às Slices: Por que elas são fundamentais?
Em Go, slices são o tipo de coleção mais versátil e amplamente utilizado. Diferentemente dos arrays — que têm tamanho fixo definido em tempo de compilação — as slices são dinâmicas e podem crescer ou encolher conforme necessário. Enquanto [5]int é um array de exatamente 5 inteiros, []int é uma slice que pode ter qualquer tamanho.
A slice funciona como uma "visão" de um array subjacente. Isso significa que múltiplas slices podem referenciar partes do mesmo array, permitindo manipulação eficiente de dados sem cópias desnecessárias.
// Array de tamanho fixo
var arr [5]int = [5]int{1, 2, 3, 4, 5}
// Slice - tamanho dinâmico
var s []int = []int{1, 2, 3, 4, 5}
// Declaração com make
s2 := make([]int, 3) // len=3, cap=3
s3 := make([]int, 3, 5) // len=3, cap=5
// Slice literal
s4 := []int{10, 20, 30}
2. Estrutura Interna de uma Slice
Internamente, uma slice é representada por um header de três campos: um ponteiro para o array subjacente, o comprimento (len) e a capacidade (cap). Essa estrutura é o que torna as slices tão eficientes.
type sliceHeader struct {
Pointer unsafe.Pointer // ponteiro para o primeiro elemento
Len int // número de elementos visíveis
Cap int // número máximo de elementos no array subjacente
}
len: número de elementos acessíveis na slicecap: número de elementos disponíveis no array subjacente a partir do ponteiro
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:4] // s = [2, 3, 4], len=3, cap=4 (posições 1 a 4 do array)
fmt.Printf("len=%d, cap=%d\n", len(s), cap(s)) // len=3, cap=4
3. Operações Essenciais de Criação e Manipulação
Criando com make
A função make permite especificar explicitamente o comprimento e a capacidade:
// Slice com len=0, cap=10 (pré-alocada)
buffer := make([]int, 0, 10)
// Slice com len=5, cap=5 (elementos zero-inicializados)
s := make([]int, 5)
Fatiamento (Slicing)
A operação s[low:high] cria uma nova slice que referencia uma porção do array subjacente:
arr := [6]int{0, 1, 2, 3, 4, 5}
s1 := arr[1:4] // [1, 2, 3], len=3, cap=5
s2 := arr[2:5] // [2, 3, 4], len=3, cap=4
s3 := arr[:3] // [0, 1, 2], len=3, cap=6
s4 := arr[3:] // [3, 4, 5], len=3, cap=3
A sintaxe de três índices s[low:high:max] controla a capacidade da nova slice:
s5 := arr[1:4:4] // [1, 2, 3], len=3, cap=3 (max=4 significa cap=4-1=3)
Go não suporta índices negativos como Python. Tentar acessar s[-1] causa erro de compilação.
4. Crescimento Dinâmico com append
A função append é a principal forma de adicionar elementos a uma slice. Quando a capacidade é insuficiente, Go aloca um novo array subjacente com capacidade maior.
s := make([]int, 0, 2)
s = append(s, 1) // cap=2
s = append(s, 2) // cap=2
s = append(s, 3) // cap=4 (dobrou)
s = append(s, 4) // cap=4
s = append(s, 5) // cap=8 (dobrou novamente)
O algoritmo de crescimento do Go dobra a capacidade para slices pequenas (até 256 elementos) e depois adiciona aproximadamente 25% para slices maiores. É crucial sempre capturar o retorno de append, pois ele pode retornar uma slice com um array subjacente diferente:
// ERRADO: pode perder dados se append realocar
s := []int{1, 2, 3}
append(s, 4) // s ainda é [1, 2, 3]
// CORRETO
s = append(s, 4) // s agora é [1, 2, 3, 4]
5. Cópia e Compartilhamento de Memória
A função copy
copy realiza uma cópia superficial dos elementos, copiando o mínimo entre len(dst) e len(src):
src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)
n := copy(dst, src) // n=3, dst=[1, 2, 3]
Compartilhamento de array subjacente
Fatiar uma slice não copia os dados — ambas compartilham o mesmo array subjacente:
original := []int{1, 2, 3, 4, 5}
fatia := original[1:4] // [2, 3, 4]
fatia[0] = 99
fmt.Println(original[1]) // 99! Modificou o array original
Isso pode causar bugs sutis. Para evitar aliasing indesejado, use copy ou crie slices independentes:
independente := make([]int, 3)
copy(independente, original[1:4])
independente[0] = 100
fmt.Println(original[1]) // 99 (inalterado)
6. Slices Nulas vs. Slices Vazias
Go distingue entre slices nulas e vazias:
var s1 []int // nil slice, len=0, cap=0
s2 := []int{} // empty slice, len=0, cap=0
s3 := make([]int, 0) // empty slice, len=0, cap=0
Diferenças importantes:
fmt.Println(s1 == nil) // true
fmt.Println(s2 == nil) // false
// Ambas funcionam com append e range
s1 = append(s1, 1) // [1]
s2 = append(s2, 1) // [1]
for range s1 { } // funciona (0 iterações)
Quando usar cada uma:
- Slice nil: valor zero para slices, ideal para retorno de funções que podem falhar ou quando não há dados
- Slice vazia: útil para JSON (serializa como [] em vez de null) e quando você quer distinguir "sem slice" de "slice vazia"
func getItems() []int {
if erro {
return nil // slice nil: "não há dados para retornar"
}
return []int{} // slice vazia: "há dados, mas nenhum elemento"
}
7. Boas Práticas e Padrões Comuns
Pré-alocação com capacidade
Evite realocações desnecessárias especificando a capacidade esperada:
// Ineficiente: append pode realocar várias vezes
func collect(items []int) []int {
var result []int
for _, item := range items {
result = append(result, item*2)
}
return result
}
// Eficiente: pré-aloca capacidade
func collectEfficient(items []int) []int {
result := make([]int, 0, len(items))
for _, item := range items {
result = append(result, item*2)
}
return result
}
Passando slices para funções
Slices são passadas por valor, mas o header contém um ponteiro para o array subjacente. Modificar elementos altera o array original, mas mudar o header (como append que realoca) não afeta o caller:
func modify(s []int) {
s[0] = 100 // visível externamente
s = append(s, 200) // não visível externamente (se realocar)
}
func main() {
s := []int{1, 2, 3}
modify(s)
fmt.Println(s) // [100, 2, 3]
}
Slice como buffer reutilizável
Resetar uma slice sem realocar memória:
buffer := make([]int, 0, 1024)
// Para reutilizar o buffer
buffer = buffer[:0] // len=0, cap=1024 (mantém o array subjacente)
Isso é extremamente útil em parsers, processamento de rede e loops de alto desempenho.
Referências
- Go Slices: usage and internals — Artigo oficial do blog Go explicando o funcionamento interno das slices, com exemplos práticos
- Effective Go: Slices — Seção do guia "Effective Go" com boas práticas e padrões de uso de slices
- Go by Example: Slices — Tutorial interativo com exemplos comentados de criação, manipulação e operações com slices
- The Go Programming Language Specification: Slice types — Especificação oficial da linguagem sobre tipos slice, incluindo sintaxe de fatiamento e regras de bounds
- Understanding Go Slices: A Comprehensive Guide — Artigo técnico detalhado sobre slices, com diagramas e casos de uso avançados
- Go Slices: Tricks and Gotchas — Guia prático com armadilhas comuns e truques para evitar bugs com slices