Slices são uma das estruturas de dados mais poderosas e flexíveis em Go, mas seu comportamento avançado pode surpreender programadores inexperientes. Nesta aula, vamos explorar tópicos fundamentais para dominar slices: como o array subjacente é compartilhado entre slices, a função copy para cópias seguras, truques para manipulação eficiente (remoção, inserção) e as pegadinhas que podem levar a bugs. Com exemplos práticos, você entenderá os mecanismos internos e evitará armadilhas comuns.

Antes de começar, lembre-se de que um slice é uma visão de um array subjacente. Isso significa que múltiplos slices podem referenciar o mesmo array, e modificações podem se propagar de formas inesperadas. Vamos começar com o compartilhamento do backing array.

Compartilhamento de backing array

Quando você cria um slice a partir de outro slice ou de um array, ambos compartilham o mesmo array subjacente (backing array). Isso é eficiente, mas pode causar efeitos colaterais se um dos slices modificar elementos que o outro espera que estejam inalterados.

Considere o exemplo a seguir:

package main

import "fmt"

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

    // Modificando s1
    s1[2] = 99
    fmt.Println("arr:", arr) // [1 2 99 4 5]
    fmt.Println("s2:", s2)   // [99 4 5]

    // Modificando s2
    s2[0] = 77
    fmt.Println("s1:", s1) // [1 2 77]
    fmt.Println("arr:", arr) // [1 2 77 4 5]
}

Veja que s1[2] e s2[0] apontam para o mesmo elemento do array (arr[2]). Alterar um afeta o outro. Esse compartilhamento é intencional e útil para evitar cópias desnecessárias, mas exige cuidado.

Outro ponto importante: quando um slice é expandido além de sua capacidade (usando append), o Go pode alocar um novo array subjacente, copiando os elementos. Isso quebra o compartilhamento. Por exemplo:

s1 := []int{1, 2, 3}
s2 := s1
s1 = append(s1, 4) // s1 agora tem novo array, s2 ainda aponta para o antigo
s1[0] = 99
fmt.Println(s1) // [99 2 3 4]
fmt.Println(s2) // [1 2 3] (inalterado)

Portanto, sempre que você usa append e o slice pode crescer, o comportamento de compartilhamento pode mudar. Para evitar surpresas, use copy quando precisar de independência total.

copy

A função embutida copy copia elementos de um slice de origem para um slice de destino. A quantidade de elementos copiados é o mínimo entre os comprimentos dos dois slices. É útil para criar cópias independentes ou para preencher slices de forma eficiente.

Sintaxe: copy(dst, src []T) int — retorna o número de elementos copiados.

package main

import "fmt"

func main() {
    src := []int{1, 2, 3, 4, 5}
    dst := make([]int, 3) // comprimento 3
    n := copy(dst, src)
    fmt.Println("copiados:", n) // 3
    fmt.Println("dst:", dst)    // [1 2 3]

    // Copiar para um slice maior
    dst2 := make([]int, 7)
    n2 := copy(dst2, src)
    fmt.Println("copiados:", n2) // 5
    fmt.Println("dst2:", dst2)   // [1 2 3 4 5 0 0]

    // Copiar parte de um slice
    src2 := []int{10, 20, 30}
    copy(dst[1:], src2) // copia a partir do índice 1 de dst
    fmt.Println("dst:", dst) // [1 10 20]
}

Note que copy não cria o slice de destino; ele deve já existir com espaço suficiente. Se o destino for menor, apenas os primeiros elementos são copiados. Além disso, copy funciona mesmo se os slices se sobreporem (origem e destino compartilham o mesmo array), comportando-se corretamente.

Para criar uma cópia completa e independente de um slice, faça:

original := []int{1, 2, 3}
copia := make([]int, len(original))
copy(copia, original)
// ou, de forma mais concisa:
copia2 := append([]int(nil), original...)

Ambas as formas criam um novo array subjacente.

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

Manipular slices sem alocar novos arrays (quando possível) é uma habilidade importante. Aqui estão truques comuns para remoção e inserção de elementos.

Remoção de um elemento no índice i:

s := []int{1, 2, 3, 4, 5}
i := 2 // remover o elemento 3
s = append(s[:i], s[i+1:]...)
fmt.Println(s) // [1 2 4 5]

Isso funciona concatenando as partes antes e depois do índice. Cuidado: se o slice tiver capacidade extra, o elemento removido ainda estará no array subjacente, mas fora do comprimento do slice. Se você quiser zerar o elemento removido para evitar vazamento de memória (se for um ponteiro), faça s[len(s)-1] = nil antes de reduzir o comprimento.

Remoção sem preservar ordem (troca com o último):

s := []int{1, 2, 3, 4, 5}
i := 2
s[i] = s[len(s)-1]
s = s[:len(s)-1]
fmt.Println(s) // [1 2 5 4] (ordem alterada)

Esse truque é O(1) e útil quando a ordem não importa.

Inserção de um elemento no índice i:

s := []int{1, 2, 4, 5}
i := 2 // inserir 3 na posição 2
s = append(s, 0) // aumentar o slice
copy(s[i+1:], s[i:]) // deslocar para a direita
s[i] = 3
fmt.Println(s) // [1 2 3 4 5]

Uma alternativa mais concisa com append:

s = append(s[:i], append([]int{3}, s[i:]...)...)

Essa forma cria um slice temporário, então não é tão eficiente, mas é comum em código. Para desempenho, prefira a primeira abordagem.

Inserção de múltiplos elementos:

s := []int{1, 2, 5}
novos := []int{3, 4}
i := 2
s = append(s, novos...) // aumenta
copy(s[i+len(novos):], s[i:])
copy(s[i:], novos)
fmt.Println(s) // [1 2 3 4 5]

Esses truques são poderosos, mas lembre-se de que podem modificar o array subjacente. Se outros slices compartilham o mesmo array, eles podem ser afetados.

Pegadinhas

Vamos listar as armadilhas mais comuns ao trabalhar com slices em Go.

1. Compartilhamento inesperado

Como vimos, slices derivados do mesmo array compartilham dados. Um erro comum é modificar um slice e esperar que outro não seja afetado.

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

2. Append pode quebrar o compartilhamento

Se você usar append em um slice que ainda tem capacidade extra, ele pode modificar o array subjacente, afetando outros slices. Se a capacidade for excedida, um novo array é alocado.

s1 := make([]int, 3, 5) // len=3, cap=5
s1[0], s1[1], s1[2] = 1, 2, 3
s2 := s1[:2]
s1 = append(s1, 4) // ainda dentro da capacidade, modifica o mesmo array
fmt.Println(s2) // [1 2] (s2 não vê o 4, mas o array subjacente foi alterado)
// Se s2 fosse expandido, veria o 4?
s2 = s2[:3]
fmt.Println(s2) // [1 2 4] (sim, porque s2 agora inclui o elemento que foi adicionado via s1)

3. Slice nil vs slice vazio

Um slice nil (var s []int) não tem array subjacente, enquanto um slice vazio (s := []int{}) tem um array de tamanho zero. append funciona com ambos, mas len e cap retornam 0. No entanto, um slice nil é frequentemente usado para indicar ausência de dados, enquanto um slice vazio representa uma coleção vazia. Isso pode causar diferenças em serialização (JSON, por exemplo) e em comparações (nil vs vazio).

var s1 []int // nil
s2 := []int{} // vazio
fmt.Println(s1 == nil) // true
fmt.Println(s2 == nil) // false
fmt.Println(len(s1), len(s2)) // 0 0

4. Vazamento de memória

Se você pegar um slice de um array grande e descartar o array, mas manter o slice, o array grande ainda será mantido na memória porque o slice faz referência a ele. Para liberar a memória, copie os elementos necessários para um novo slice.

func pegarPequenoSlice() []int {
    grandeArray := make([]int, 1000000)
    // ... preenche
    return grandeArray[0:3] // retorna um slice que mantém o array gigante
}

Nesse caso, o array de 1 milhão de inteiros não será liberado enquanto o slice retornado existir. Solução:

func pegarPequenoSlice() []int {
    grandeArray := make([]int, 1000000)
    // ... preenche
    pequeno := make([]int, 3)
    copy(pequeno, grandeArray[:3])
    return pequeno
}

5. Modificar slice durante iteração com range

Se você modificar um slice (adicionar ou remover elementos) durante um loop range, o comportamento pode ser inesperado porque o range avalia o comprimento uma vez no início.

s := []int{1, 2, 3, 4}
for i, v := range s {
    if v == 2 {
        s = append(s, 5) // isso não afeta a iteração atual
    }
    fmt.Println(i, v)
}
// imprime 0 1, 1 2, 2 3, 3 4 (não vê o 5)

Se você precisar modificar o slice durante a iteração, use um índice manual.

Boas práticas

  • Sempre prefira copy ou append com slice vazio para criar cópias independentes.
  • Evite modificar slices que são compartilhados com outras partes do código, a menos que seja intencional.
  • Use make com capacidade adequada para evitar realocações desnecessárias.
  • Para slices de ponteiros, lembre-se de nilificar os elementos removidos para evitar vazamento de memória.
  • Documente quando uma função retorna um slice que compartilha o array subjacente com o argumento.

Referências

Exercícios

  1. Escreva uma função que receba um slice de inteiros e um índice i, e remova o elemento no índice i preservando a ordem. A função deve retornar o novo slice.

    ✓ Resposta:
    func remove(s []int, i int) []int {
        return append(s[:i], s[i+1:]...)
    }
  2. Dado um slice s := []int{1,2,3,4,5}, crie uma cópia independente (novo array subjacente) usando copy e depois usando append.

    ✓ Resposta:
    s := []int{1,2,3,4,5}
    // com copy:
    copia1 := make([]int, len(s))
    copy(copia1, s)
    // com append:
    copia2 := append([]int(nil), s...)
  3. O que o seguinte código imprime? Explique.

    arr := [3]int{1,2,3}
    s1 := arr[:]
    s2 := arr[:2]
    s1[0] = 99
    fmt.Println(s2[0])

    ✓ Resposta:

    Imprime 99, porque s1 e s2 compartilham o mesmo array subjacente (arr). Alterar s1[0] altera arr[0], que é o mesmo que s2[0].

  4. Escreva uma função que insira o valor 10 no índice 2 do slice []int{1,2,3,4,5} sem usar append com slice temporário. Use a técnica de deslocamento com copy.

    ✓ Resposta:
    s := []int{1,2,3,4,5}
    i := 2
    s = append(s, 0) // aumenta o comprimento
    copy(s[i+1:], s[i:]) // desloca para a direita
    s[i] = 10
    fmt.Println(s) // [1 2 10 3 4 5]
  5. Explique por que o código abaixo pode causar vazamento de memória e como corrigi-lo.

    func getFirstThree() []int {
        big := make([]int, 1000000)
        // preenche big
        return big[:3]
    }

    ✓ Resposta:

    O slice retornado mantém uma referência ao array grande (big), impedindo que o garbage collector o libere. Para corrigir, copie os três primeiros elementos para um novo slice:

    func getFirstThree() []int {
        big := make([]int, 1000000)
        // preenche big
        small := make([]int, 3)
        copy(small, big[:3])
        return small
    }