Slices avançado
Esta aula aprofunda o uso de slices em Go, abordando o compartilhamento do array subjacente, a função copy, truques comuns como remoção e inserção de elementos, e pegadinhas frequentes que podem causar bugs sutis. O conteúdo inclui exemplos práticos e exercícios para fixação.
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
copyouappendcom 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
makecom 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
- Go Blog: Arrays, slices (and strings)
- Go Spec: Slice types
- Go Spec: Appending and copying slices
- Package builtin: copy
- Go Wiki: SliceTricks
- YourBasic: Slice tricks
Exercícios
-
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:]...) } -
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...) -
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].
-
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] -
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 }