Slices são uma das estruturas de dados mais importantes e flexíveis em Go. Diferente de arrays, que têm tamanho fixo, slices são dinâmicos e podem crescer ou encolher conforme necessário. Nesta aula, vamos aprofundar o funcionamento dos slices, suas propriedades, operações e armadilhas comuns.

Um slice é uma abstração sobre um array subjacente. Ele fornece uma visão de uma parte (ou de todo) desse array, permitindo manipulação eficiente sem copiar dados. Internamente, um slice é composto por três campos: um ponteiro para o array, o comprimento (len) e a capacidade (cap).

Diferença de arrays

Arrays em Go têm tamanho fixo definido em tempo de compilação. Por exemplo, var a [5]int declara um array de 5 inteiros. Você não pode adicionar ou remover elementos; o tamanho é parte do tipo. Já slices não têm tamanho fixo; eles são dinâmicos e podem crescer. Um slice é declarado sem especificar o tamanho: var s []int.

Outra diferença importante é que arrays são valores: quando você atribui ou passa um array para uma função, ele é copiado inteiramente. Slices, por outro lado, são referências: eles apontam para um array subjacente. Atribuir um slice não copia os dados, apenas cria outra referência ao mesmo array.

package main

import "fmt"

func main() {
    // Array: tamanho fixo
    var arr [3]int = [3]int{1, 2, 3}
    fmt.Println("Array:", arr)

    // Slice: dinâmico
    var slc []int = []int{1, 2, 3}
    fmt.Println("Slice:", slc)

    // Atribuição: array copia, slice referencia
    arr2 := arr
    arr2[0] = 100
    fmt.Println("Array original:", arr) // [1 2 3]

    slc2 := slc
    slc2[0] = 100
    fmt.Println("Slice original:", slc) // [100 2 3] (mesmo array subjacente)
}

len e cap

Todo slice possui duas propriedades fundamentais: comprimento (len) e capacidade (cap). O comprimento é o número de elementos que o slice contém atualmente. A capacidade é o número máximo de elementos que o slice pode armazenar sem realocar o array subjacente. Esses valores podem ser obtidos com as funções nativas len() e cap().

A capacidade de um slice é determinada pelo array subjacente a partir da posição inicial do slice. Se você criar um slice a partir de um array, a capacidade será o número de elementos do início do slice até o final do array. Por exemplo, s := arr[1:3] tem comprimento 2 e capacidade igual a len(arr) - 1.

package main

import "fmt"

func main() {
    arr := [5]int{10, 20, 30, 40, 50}
    s := arr[1:4] // elementos 20, 30, 40
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) // len=3 cap=4

    // Alterando o slice não afeta a capacidade inicial
    s = s[:2] // reduz comprimento, capacidade permanece 4
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) // len=2 cap=4
}

append

A função append é usada para adicionar elementos a um slice. Ela recebe o slice e os elementos a serem adicionados, e retorna um novo slice (ou o mesmo, se houver capacidade suficiente). Se a capacidade for insuficiente, append aloca um novo array subjacente com maior capacidade, copia os dados existentes e adiciona os novos elementos.

É importante atribuir o resultado de append de volta à variável do slice, pois o slice original pode ser inválido após a realocação. O crescimento da capacidade segue uma estratégia de duplicação para slices pequenos e um fator de crescimento menor para slices grandes.

package main

import "fmt"

func main() {
    var s []int
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) // len=0 cap=0 []

    s = append(s, 1)
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) // len=1 cap=1 [1]

    s = append(s, 2, 3, 4)
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) // len=4 cap=4 [1 2 3 4]

    s = append(s, 5)
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) // len=5 cap=8 [1 2 3 4 5]
}

Slice de slice e armadilhas

Você pode criar slices a partir de outros slices, usando a mesma sintaxe de fatiamento. Por exemplo, sub := s[1:3] cria um novo slice que referencia o mesmo array subjacente do slice original. Isso significa que alterações em um slice podem afetar o outro, se eles compartilharem o mesmo array.

Uma armadilha comum ocorre quando você usa append em um sub-slice. Se a capacidade do sub-slice for suficiente, o append pode sobrescrever elementos do slice original. Por exemplo, se você tem s := []int{1,2,3,4} e cria sub := s[:2] (capacidade 4), fazer sub = append(sub, 100) modificará o terceiro elemento de s (de 3 para 100). Isso pode causar bugs sutis.

package main

import "fmt"

func main() {
    s := []int{1, 2, 3, 4}
    sub := s[:2] // sub = [1,2], cap=4
    fmt.Println("Antes:", s, sub)

    sub = append(sub, 100) // modifica s[2]
    fmt.Println("Depois:", s, sub) // s = [1,2,100,4], sub = [1,2,100]

    // Para evitar, use copy ou crie um slice com capacidade igual ao comprimento
    sub2 := make([]int, 2)
    copy(sub2, s[:2])
    sub2 = append(sub2, 200)
    fmt.Println("Com copy:", s, sub2) // s permanece [1,2,100,4]
}

Boas práticas

Sempre atribua o resultado de append à mesma variável. Evite compartilhar slices entre funções sem entender o impacto na memória. Use copy para criar slices independentes quando necessário. Lembre-se de que slices são referências: se você precisar de uma cópia dos dados, use copy ou append com slice vazio.

Referências

Exercícios

  1. Crie um slice de inteiros com os valores 1, 2, 3, 4, 5. Use append para adicionar o valor 6. Imprima o slice.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    func main() {
        s := []int{1, 2, 3, 4, 5}
        s = append(s, 6)
        fmt.Println(s) // [1 2 3 4 5 6]
    }
  2. Dado o array arr := [5]int{10, 20, 30, 40, 50}, crie um slice que contenha os elementos do índice 1 ao 3 (exclusive 3). Imprima o comprimento e a capacidade do slice.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    func main() {
        arr := [5]int{10, 20, 30, 40, 50}
        s := arr[1:3]
        fmt.Printf("len=%d cap=%d\n", len(s), cap(s)) // len=2 cap=4
    }
  3. Explique o que acontece quando você faz append em um slice que já atingiu sua capacidade. Como o Go lida com isso?

    ✓ Resposta: Quando a capacidade é insuficiente, append aloca um novo array subjacente com capacidade maior (geralmente dobrando para slices pequenos), copia os elementos existentes para o novo array e adiciona os novos. O slice retornado aponta para o novo array. O array antigo pode ser coletado pelo garbage collector se não houver mais referências.
  4. Crie um slice s := []int{1, 2, 3, 4, 5}. Crie um sub-slice sub := s[1:4]. Modifique o elemento de índice 2 de sub para 99. Imprima ambos os slices. O que você observa?

    ✓ Resposta:
    package main
    
    import "fmt"
    
    func main() {
        s := []int{1, 2, 3, 4, 5}
        sub := s[1:4] // [2,3,4]
        sub[2] = 99   // modifica o índice 2 de sub (que é o índice 3 de s)
        fmt.Println("s:", s)   // [1 2 3 99 5]
        fmt.Println("sub:", sub) // [2 3 99]
    }
    Observação: Ambos os slices compartilham o mesmo array subjacente, então a modificação em sub afeta s.
  5. Escreva uma função que receba um slice de inteiros e retorne um novo slice contendo apenas os números pares. Use append.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    func filterEven(s []int) []int {
        var result []int
        for _, v := range s {
            if v%2 == 0 {
                result = append(result, v)
            }
        }
        return result
    }
    
    func main() {
        s := []int{1, 2, 3, 4, 5, 6}
        fmt.Println(filterEven(s)) // [2 4 6]
    }