Nesta aula, vamos explorar conceitos avançados de slices em Go, fundamentais para escrever código eficiente e evitar erros sutis. Slices são uma abstração sobre arrays, mas seu comportamento de compartilhamento de memória e mutabilidade exige atenção. Dominar esses tópicos é essencial para programadores Go que desejam manipular coleções de dados com segurança e desempenho.

Começaremos entendendo como slices compartilham o mesmo array subjacente (backing array), depois veremos a função copy para duplicar dados, truques para remoção e inserção de elementos, e por fim, as pegadinhas mais comuns que podem levar a bugs difíceis de detectar.

Compartilhamento de backing array

Em Go, um slice é uma estrutura que contém um ponteiro para um array, um comprimento (length) e uma capacidade (capacity). Quando criamos um slice a partir de outro, ambos apontam para o mesmo array subjacente. Isso significa que modificações nos elementos de um slice podem afetar o outro, desde que estejam dentro da faixa de comprimento de cada um.

Por exemplo, se temos um slice s1 := []int{1, 2, 3, 4, 5} e criamos s2 := s1[1:3], ambos compartilham o mesmo array. Alterar s2[0] altera s1[1]. Esse comportamento é eficiente, mas pode causar efeitos colaterais indesejados se não for compreendido.

package main

import "fmt"

func main() {
    s1 := []int{1, 2, 3, 4, 5}
    s2 := s1[1:3] // [2, 3]
    s2[0] = 99
    fmt.Println(s1) // [1, 99, 3, 4, 5]
    fmt.Println(s2) // [99, 3]
}

Para evitar compartilhamento, podemos usar a função copy ou criar um slice completamente novo com append e elementos individuais.

copy

A função embutida copy copia elementos de um slice origem para um slice destino, retornando o número de elementos copiados. O número de elementos copiados é o mínimo entre len(dest) e len(src). É importante que o destino tenha espaço suficiente (geralmente usamos make para alocar).

package main

import "fmt"

func main() {
    src := []int{1, 2, 3}
    dest := make([]int, len(src))
    n := copy(dest, src)
    fmt.Println(dest, n) // [1 2 3] 3
}

Se o destino for menor, apenas os primeiros elementos são copiados. Se for maior, o restante permanece zero. copy também funciona com slices que compartilham o mesmo array, mas é seguro e não causa problemas de overlapping (a implementação lida corretamente).

Truques (remoção, inserção)

Manipular slices sem criar um novo array subjacente é comum. Para remover um elemento no índice i, podemos usar append com os elementos antes e depois. A ordem importa: para remover sem preservar a ordem, podemos trocar com o último e truncar.

// Remover elemento no índice i (ordem preservada)
s := []int{1, 2, 3, 4, 5}
i := 2
s = append(s[:i], s[i+1:]...)
fmt.Println(s) // [1 2 4 5]

// Remover sem preservar ordem (rápido)
s[i] = s[len(s)-1]
s = s[:len(s)-1]
fmt.Println(s) // [1 2 5 4] (ordem alterada)

Para inserir um elemento no índice i, podemos usar append com uma slice vazia para aumentar a capacidade e depois copiar:

s := []int{1, 2, 3}
i := 1
s = append(s[:i], append([]int{99}, s[i:]...)...)
fmt.Println(s) // [1 99 2 3]

Outra forma eficiente é usar copy para deslocar elementos:

s = append(s, 0) // aumenta o slice
copy(s[i+1:], s[i:]) // desloca
s[i] = 99
fmt.Println(s) // [1 99 2 3]

Pegadinhas

Algumas armadilhas comuns: 1) Compartilhamento inesperado: ao passar um slice para uma função e modificá-lo dentro do comprimento, o slice original pode ser alterado. 2) Append e capacidade: se o append não exceder a capacidade, ele modifica o array subjacente; se exceder, aloca um novo array, e o slice original não é afetado. 3) Re-slicing além da capacidade: s[:cap(s)] é válido, mas s[:cap(s)+1] causa pânico. 4) Uso de nil: um slice nil tem comprimento e capacidade zero, mas append funciona e aloca um novo array.

package main

import "fmt"

func main() {
    s := []int{1, 2, 3}
    modSlice(s)
    fmt.Println(s) // [99 2 3] (compartilhamento)

    var nilSlice []int
    nilSlice = append(nilSlice, 1)
    fmt.Println(nilSlice) // [1]
}

func modSlice(s []int) {
    s[0] = 99
}

Outra pegadinha: ao usar append em uma função, a variável original pode não ser atualizada se a capacidade for excedida (pois o slice passado é uma cópia da estrutura, o ponteiro pode mudar). Sempre retorne o slice ou use ponteiro.

Boas práticas

Prefira usar copy para duplicar slices quando necessário. Ao remover ou inserir, considere a eficiência: remover sem preservar ordem é O(1), com preservação é O(n). Evite confiar no compartilhamento de backing array a menos que seja intencional. Documente quando uma função modifica o slice original.

Referências

Exercícios

  1. Crie um slice a com os números 1 a 5. Crie um slice b a partir de a[1:4]. Altere b[0] para 99 e imprima a. Explique o resultado.

    ✓ Resposta:
    a := []int{1, 2, 3, 4, 5}
    b := a[1:4]
    b[0] = 99
    fmt.Println(a) // [1 99 3 4 5]
    O resultado é [1 99 3 4 5] porque b compartilha o mesmo array subjacente que a. Alterar b[0] modifica o elemento no índice 1 de a.
  2. Escreva uma função que recebe um slice de inteiros e remove o elemento no índice 2, preservando a ordem. Teste com []int{10, 20, 30, 40, 50}.

    ✓ Resposta:
    func remove(s []int, i int) []int {
        return append(s[:i], s[i+1:]...)
    }
    
    func main() {
        s := []int{10, 20, 30, 40, 50}
        s = remove(s, 2)
        fmt.Println(s) // [10 20 40 50]
    }
  3. Use a função copy para criar uma cópia independente do slice original := []int{1, 2, 3}. Modifique a cópia e verifique que o original não é alterado.

    ✓ Resposta:
    original := []int{1, 2, 3}
    copia := make([]int, len(original))
    copy(copia, original)
    copia[0] = 99
    fmt.Println(original) // [1 2 3]
    fmt.Println(copia)    // [99 2 3]
  4. Explique por que o código abaixo pode causar um bug e como corrigi-lo:

    func main() {
        s := []int{1, 2, 3}
        appendAndModify(s)
        fmt.Println(s)
    }
    
    func appendAndModify(s []int) {
        s = append(s, 4)
        s[0] = 99
    }

    ✓ Resposta: O bug é que a função appendAndModify recebe uma cópia do slice header. Se o append exceder a capacidade, o slice local aponta para um novo array, e as modificações não afetam o slice original. Para corrigir, retorne o slice modificado ou use um ponteiro. Exemplo corrigido: func appendAndModify(s *[]int) { *s = append(*s, 4); (*s)[0] = 99 }.
  5. Crie um slice nums := []int{1, 2, 3, 4, 5}. Insira o valor 100 no índice 2 usando a técnica de append com slice vazio e depois com copy. Compare as duas abordagens.

    ✓ Resposta:
    // Abordagem 1: append com slice vazio
    nums := []int{1, 2, 3, 4, 5}
    nums = append(nums[:2], append([]int{100}, nums[2:]...)...)
    fmt.Println(nums) // [1 2 100 3 4 5]
    
    // Abordagem 2: usando copy
    nums = []int{1, 2, 3, 4, 5}
    nums = append(nums, 0) // aumenta a capacidade
    copy(nums[3:], nums[2:])
    nums[2] = 100
    fmt.Println(nums) // [1 2 100 3 4 5]
    A primeira abordagem é mais concisa, mas aloca um novo slice temporário. A segunda é mais eficiente por evitar alocação extra, mas requer mais linhas.