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 slice
  • cap: 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