Slices avançado
Esta aula aprofunda o uso de slices em Go, abordando o compartilhamento de backing array, a função copy, truques comuns como remoção e inserção, e pegadinhas frequentes. O conteúdo inclui exemplos práticos e exercícios para consolidar o aprendizado.
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
- Go Blog: Arrays, slices (and strings)
- Go Specification: Slice types
- Go Doc: builtin.copy
- Go Wiki: Slice Tricks
- DigitalOcean: Understanding Arrays and Slices in Go
- YourBasic: Slice internals and tricks
Exercícios
Crie um slice
acom os números 1 a 5. Crie um sliceba partir dea[1:4]. Altereb[0]para 99 e imprimaa. Explique o resultado.✓ Resposta:
O resultado é [1 99 3 4 5] porquea := []int{1, 2, 3, 4, 5} b := a[1:4] b[0] = 99 fmt.Println(a) // [1 99 3 4 5]bcompartilha o mesmo array subjacente quea. Alterarb[0]modifica o elemento no índice 1 dea.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] }Use a função
copypara criar uma cópia independente do sliceoriginal := []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]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çãoappendAndModifyrecebe uma cópia do slice header. Se oappendexceder 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 }.Crie um slice
nums := []int{1, 2, 3, 4, 5}. Insira o valor 100 no índice 2 usando a técnica deappendcom slice vazio e depois comcopy. Compare as duas abordagens.✓ Resposta:
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.// 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]