Operações com slices: append, copy e crescimento

1. A Função append: Adicionando Elementos Dinamicamente

A função append é a principal ferramenta para adicionar elementos a um slice em Go. Sua sintaxe é direta:

slice = append(slice, elementos...)

O comportamento do append depende da capacidade atual do slice. Se houver espaço suficiente no array subjacente, os elementos são adicionados diretamente. Caso contrário, o Go aloca um novo array com capacidade maior, copia os elementos existentes e então adiciona os novos.

package main

import "fmt"

func main() {
    // Slice com capacidade 3
    frutas := make([]string, 0, 3)
    frutas = append(frutas, "maçã", "banana", "laranja")
    fmt.Printf("len=%d cap=%d %v\n", len(frutas), cap(frutas), frutas)

    // Este append causará realocação (capacidade insuficiente)
    frutas = append(frutas, "uva")
    fmt.Printf("len=%d cap=%d %v\n", len(frutas), cap(frutas), frutas)
}

Para concatenar slices, usamos o operador ...:

slice1 := []int{1, 2, 3}
slice2 := []int{4, 5, 6}
combinado := append(slice1, slice2...)
fmt.Println(combinado) // [1 2 3 4 5 6]

2. O Mecanismo de Crescimento Interno do Slice

O Go utiliza uma estratégia inteligente para gerenciar o crescimento de slices. Para slices pequenos (tipicamente até 256 elementos), a capacidade dobra a cada realocação. Para slices maiores, o fator de crescimento é reduzido para evitar desperdício de memória.

package main

import "fmt"

func main() {
    slice := make([]int, 0)
    for i := 0; i < 10; i++ {
        slice = append(slice, i)
        fmt.Printf("Após adicionar %d: len=%d cap=%d\n", i, len(slice), cap(slice))
    }
}

Em versões recentes do Go (1.18+), o algoritmo de crescimento é:

  • Para capacidade < 256: dobra o tamanho (cap *= 2)
  • Para capacidade >= 256: cresce em aproximadamente 25% (cap += cap/4 + 192)

Isso significa que realocações frequentes têm custo O(n) devido à cópia de elementos, tornando a pré-alocação uma prática importante para slices grandes.

3. A Função copy: Copiando Dados entre Slices

A função copy permite transferir elementos entre slices de forma eficiente:

copiados := copy(destino, origem)

O número de elementos copiados é o mínimo entre len(origem) e len(destino):

package main

import "fmt"

func main() {
    origem := []int{1, 2, 3, 4, 5}
    destino := make([]int, 3)

    copiados := copy(destino, origem)
    fmt.Printf("Copiados: %d\n", copiados) // 3
    fmt.Println(destino) // [1 2 3]

    // Cópia parcial usando slices de expressão
    destino2 := make([]int, 5)
    copy(destino2[1:4], origem[2:5])
    fmt.Println(destino2) // [0 3 4 5 0]
}

4. Diferenças Cruciais entre append e copy

A principal diferença está no gerenciamento de memória:

  • append pode modificar o array subjacente e realocar
  • copy nunca altera o tamanho do slice destino
package main

import "fmt"

func main() {
    // Perigo: append pode causar efeitos colaterais
    original := []int{1, 2, 3, 4, 5}
    sub := original[:3]

    // Se append não realocar, modifica o array compartilhado
    sub = append(sub, 10)
    fmt.Println(original) // Pode ser [1 2 3 10 5] ou [1 2 3 4 5] dependendo da realocação

    // copy é seguro - não altera o slice original
    copia := make([]int, 3)
    copy(copia, original)
    copia[0] = 99
    fmt.Println(original) // [1 2 3 4 5] - inalterado
}

Use append quando precisar crescer o slice. Use copy quando precisar duplicar dados sem modificar o tamanho.

5. Gerenciamento de Capacidade: make e Pré-alocação

A função make permite criar slices com capacidade pré-definida:

slice := make([]T, comprimento, capacidade)

A pré-alocação é crucial para performance em loops:

package main

import (
    "fmt"
    "time"
)

func main() {
    n := 1000000

    // Sem pré-alocação
    start := time.Now()
    s1 := []int{}
    for i := 0; i < n; i++ {
        s1 = append(s1, i)
    }
    fmt.Printf("Sem pré-alocação: %v\n", time.Since(start))

    // Com pré-alocação
    start = time.Now()
    s2 := make([]int, 0, n)
    for i := 0; i < n; i++ {
        s2 = append(s2, i)
    }
    fmt.Printf("Com pré-alocação: %v\n", time.Since(start))
}

A diferença é significativa — a pré-alocação elimina múltiplas realocações e cópias de dados.

6. Técnicas Avançadas com append e copy

Removendo elementos sem perder ordem:

func remove(slice []int, i int) []int {
    return append(slice[:i], slice[i+1:]...)
}

func removePreservandoOrdem(slice []int, i int) []int {
    copy(slice[i:], slice[i+1:])
    return slice[:len(slice)-1]
}

Inserindo elementos no meio:

func insert(slice []int, i int, value int) []int {
    slice = append(slice, 0)
    copy(slice[i+1:], slice[i:])
    slice[i] = value
    return slice
}

Implementando uma pilha eficiente:

type Stack []int

func (s *Stack) Push(v int) {
    *s = append(*s, v)
}

func (s *Stack) Pop() (int, bool) {
    if len(*s) == 0 {
        return 0, false
    }
    v := (*s)[len(*s)-1]
    *s = (*s)[:len(*s)-1]
    return v, true
}

7. Armadilhas Comuns e Boas Práticas

Armadilha 1: Compartilhamento acidental do array subjacente

a := []int{1, 2, 3, 4, 5}
b := a[:2]
b = append(b, 99) // Pode modificar a se não houver realocação

Solução: Sempre fazer uma cópia explícita quando precisar de independência:

b := make([]int, 2)
copy(b, a[:2])

Armadilha 2: Confundir len e cap ao usar copy

destino := make([]int, 0, 10) // len = 0
copy(destino, origem) // Não copia nada! destino tem len=0

Solução: Criar o destino com len igual ao número de elementos desejados:

destino := make([]int, len(origem))
copy(destino, origem)

Recomendações finais:

  1. Sempre reatribua o resultado de append: slice = append(slice, elem)
  2. Pré-aloque capacidade quando o tamanho final for conhecido
  3. Use copy para duplicar dados de forma segura
  4. Documente realocações em código crítico de performance
  5. Prefira slices de expressão para criar sub-slices sem realocar

Referências