Slices são uma das estruturas de dados mais importantes em Go. Diferentemente de arrays, que têm tamanho fixo, slices são dinâmicos e oferecem uma interface flexível para trabalhar com sequências de elementos. Internamente, um slice é uma estrutura que referencia um array subjacente, contendo ponteiro, comprimento e capacidade. Nesta aula, vamos explorar as diferenças entre slices e arrays, as funções len e cap, o uso de append e a criação de slices a partir de slices, além de armadilhas comuns relacionadas ao compartilhamento de memória.

Dominar slices é essencial para programar eficientemente em Go, pois eles são amplamente utilizados em coleções, manipulação de dados e até mesmo como base para tipos como strings e mapas. Vamos começar!

Diferença de arrays

Em Go, arrays têm tamanho fixo definido na declaração. Por exemplo, var arr [5]int cria um array de 5 inteiros. O tamanho é parte do tipo, então [5]int e [10]int são tipos diferentes. Arrays são valores: quando passados para funções, são copiados inteiramente.

Já slices são tipos de referência. Eles não armazenam dados, apenas referenciam um array subjacente. A declaração var s []int cria um slice nil (sem array). Slices podem crescer e encolher, e são passados por referência (cópia da estrutura, mas o array subjacente é compartilhado).

package main

import "fmt"

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

    // Slice criado a partir de array
    slice := arr[:]
    fmt.Println("Slice:", slice)

    // Modificar slice afeta array subjacente
    slice[0] = 10
    fmt.Println("Array após modificar slice:", arr)
}

No exemplo, a modificação no slice reflete no array original.

len e cap

Todo slice possui dois atributos: comprimento (len) e capacidade (cap). len é o número de elementos atualmente no slice; cap é o número de elementos no array subjacente a partir do ponteiro do slice. A capacidade indica quanto o slice pode crescer sem realocar o array.

As funções len e cap são built-in e retornam esses valores. Para um slice nil, ambos são 0. A capacidade é especialmente útil ao usar append, pois evita realocações desnecessárias.

package main

import "fmt"

func main() {
    s := make([]int, 3, 5) // comprimento 3, capacidade 5
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)

    s = append(s, 4)
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

Saída: len=3 cap=5 [0 0 0] e depois len=4 cap=5 [0 0 0 4]. A capacidade permanece 5.

append

A função append adiciona elementos ao final de um slice. Se a capacidade for suficiente, ela modifica o array subjacente e retorna um slice com o novo comprimento. Se não, ela aloca um novo array, copia os dados e retorna um slice apontando para o novo array. Por isso, é importante atribuir o resultado de append de volta à variável do slice.

package main

import "fmt"

func main() {
    var s []int
    fmt.Println("Antes:", s, "len:", len(s), "cap:", cap(s))

    s = append(s, 10)
    fmt.Println("Depois:", s, "len:", len(s), "cap:", cap(s))

    // Append múltiplos elementos
    s = append(s, 20, 30, 40)
    fmt.Println("Múltiplos:", s, "len:", len(s), "cap:", cap(s))

    // Append de outro slice (desempacotando)
    outros := []int{50, 60}
    s = append(s, outros...)
    fmt.Println("Com outros:", s)
}

A saída mostra que a capacidade inicial era 0, e após append, Go alocou um novo array (geralmente dobrando a capacidade).

Slice de slice e armadilhas

Podemos criar um slice a partir de outro slice usando a sintaxe s[inicio:fim]. O novo slice compartilha o mesmo array subjacente, a menos que ocorra realocação. Isso pode levar a comportamentos inesperados se não tomarmos cuidado.

Armadilha comum: modificar um elemento no sub-slice pode afetar o slice original (e vice-versa). Além disso, ao usar append em um sub-slice, se a capacidade do sub-slice não for suficiente, um novo array é criado, mas se for suficiente, o append pode sobrescrever elementos do slice original além do seu comprimento.

package main

import "fmt"

func main() {
    original := []int{1, 2, 3, 4, 5}
    sub := original[1:3] // sub = [2, 3], len=2, cap=4 (do índice 1 ao final)
    fmt.Println("original:", original, "sub:", sub)

    // Modificar sub afeta original
    sub[0] = 20
    fmt.Println("original após modificar sub:", original)

    // Append em sub que não excede capacidade: sobrescreve elemento do original
    sub = append(sub, 30) // sub agora [20, 3, 30], original fica [1, 20, 3, 30, 5]
    fmt.Println("original após append em sub:", original)
    fmt.Println("sub:", sub)
}

Para evitar essas armadilhas, podemos usar a função copy para criar um slice independente, ou usar append com cuidado. Entender o compartilhamento de memória é crucial para escrever código seguro.

Referências

Exercícios

  1. Crie um slice de inteiros com valores 1, 2, 3, 4, 5. Use append para adicionar os valores 6, 7, 8. Imprima o slice resultante e sua capacidade.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    func main() {
        s := []int{1, 2, 3, 4, 5}
        s = append(s, 6, 7, 8)
        fmt.Println("Slice:", s)
        fmt.Println("Capacidade:", cap(s))
    }
    
  2. Dado o array arr := [5]int{10, 20, 30, 40, 50}, crie um slice que contenha apenas os elementos do índice 1 ao 3 (exclusivo). Modifique o primeiro elemento do slice para 99 e imprima o array original. Explique o que aconteceu.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    func main() {
        arr := [5]int{10, 20, 30, 40, 50}
        slice := arr[1:3] // [20, 30]
        slice[0] = 99
        fmt.Println("Array:", arr) // [10, 99, 30, 40, 50]
    }
    

    O slice compartilha o array subjacente, então modificar o slice altera o array original.

  3. Escreva um programa que crie um slice com capacidade 5 e comprimento 3, preenchido com zeros. Em seguida, use append para adicionar mais 3 elementos. Imprima o comprimento e a capacidade após cada append.

    ✓ Resposta:
    package main
    
    import "fmt"
    
    func main() {
        s := make([]int, 3, 5)
        fmt.Printf("Início: len=%d cap=%d %v\n", len(s), cap(s), s)
        s = append(s, 1)
        fmt.Printf("Após 1: len=%d cap=%d %v\n", len(s), cap(s), s)
        s = append(s, 2)
        fmt.Printf("Após 2: len=%d cap=%d %v\n", len(s), cap(s), s)
        s = append(s, 3)
        fmt.Printf("Após 3: len=%d cap=%d %v\n", len(s), cap(s), s)
    }
    

    Saída esperada: len=3 cap=5, depois len=4 cap=5, len=5 cap=5, len=6 cap=10 (dobrou).

  4. Explique por que o código a seguir pode causar um bug e como corrigi-lo:

    s := []int{1, 2, 3, 4, 5}
    sub := s[0:2]
    sub = append(sub, 100)
    fmt.Println(s) // O que imprime?
    

    ✓ Resposta:

    O código imprime [1, 2, 100, 4, 5]. O append em sub (capacidade 5) sobrescreve o terceiro elemento do array original. Para evitar, use copy para criar um slice independente:

    s := []int{1, 2, 3, 4, 5}
    sub := make([]int, 2)
    copy(sub, s[0:2])
    sub = append(sub, 100)
    fmt.Println(s) // [1,2,3,4,5]
    
  5. Crie uma função que receba um slice de inteiros e retorne um novo slice contendo apenas os números pares. Use append para construir o resultado.

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

Boas práticas

Sempre atribua o resultado de append de volta à mesma variável (ou a uma nova). Evite manter referências a slices que compartilham o mesmo array subjacente se houver risco de mutação inesperada. Use make com capacidade adequada para evitar realocações frequentes. Prefira usar copy para criar slices independentes quando necessário.

Com esses conceitos, você está pronto para usar slices de forma eficiente e segura em Go.