Slices
Esta aula explora slices em Go, sua estrutura dinâmica, funções len e cap, append e criação de sub-slices, com ênfase em armadilhas de compartilhamento de memória.
Slices são uma das estruturas de dados mais importantes em Go. Diferentemente de arrays, que têm tamanho fixo, slices são dinâmicos e oferecem uma interface flexível para trabalhar com sequências de elementos. Internamente, um slice é uma estrutura que referencia um array subjacente, contendo ponteiro, comprimento e capacidade. Nesta aula, vamos explorar as diferenças entre slices e arrays, as funções len e cap, o uso de append e a criação de slices a partir de slices, além de armadilhas comuns relacionadas ao compartilhamento de memória.
Dominar slices é essencial para programar eficientemente em Go, pois eles são amplamente utilizados em coleções, manipulação de dados e até mesmo como base para tipos como strings e mapas. Vamos começar!
Diferença de arrays
Em Go, arrays têm tamanho fixo definido na declaração. Por exemplo, var arr [5]int cria um array de 5 inteiros. O tamanho é parte do tipo, então [5]int e [10]int são tipos diferentes. Arrays são valores: quando passados para funções, são copiados inteiramente.
Já slices são tipos de referência. Eles não armazenam dados, apenas referenciam um array subjacente. A declaração var s []int cria um slice nil (sem array). Slices podem crescer e encolher, e são passados por referência (cópia da estrutura, mas o array subjacente é compartilhado).
package main
import "fmt"
func main() {
// Array de tamanho fixo
var arr [3]int = [3]int{1, 2, 3}
fmt.Println("Array:", arr)
// Slice criado a partir de array
slice := arr[:]
fmt.Println("Slice:", slice)
// Modificar slice afeta array subjacente
slice[0] = 10
fmt.Println("Array após modificar slice:", arr)
}
No exemplo, a modificação no slice reflete no array original.
len e cap
Todo slice possui dois atributos: comprimento (len) e capacidade (cap). len é o número de elementos atualmente no slice; cap é o número de elementos no array subjacente a partir do ponteiro do slice. A capacidade indica quanto o slice pode crescer sem realocar o array.
As funções len e cap são built-in e retornam esses valores. Para um slice nil, ambos são 0. A capacidade é especialmente útil ao usar append, pois evita realocações desnecessárias.
package main
import "fmt"
func main() {
s := make([]int, 3, 5) // comprimento 3, capacidade 5
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
s = append(s, 4)
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
Saída: len=3 cap=5 [0 0 0] e depois len=4 cap=5 [0 0 0 4]. A capacidade permanece 5.
append
A função append adiciona elementos ao final de um slice. Se a capacidade for suficiente, ela modifica o array subjacente e retorna um slice com o novo comprimento. Se não, ela aloca um novo array, copia os dados e retorna um slice apontando para o novo array. Por isso, é importante atribuir o resultado de append de volta à variável do slice.
package main
import "fmt"
func main() {
var s []int
fmt.Println("Antes:", s, "len:", len(s), "cap:", cap(s))
s = append(s, 10)
fmt.Println("Depois:", s, "len:", len(s), "cap:", cap(s))
// Append múltiplos elementos
s = append(s, 20, 30, 40)
fmt.Println("Múltiplos:", s, "len:", len(s), "cap:", cap(s))
// Append de outro slice (desempacotando)
outros := []int{50, 60}
s = append(s, outros...)
fmt.Println("Com outros:", s)
}
A saída mostra que a capacidade inicial era 0, e após append, Go alocou um novo array (geralmente dobrando a capacidade).
Slice de slice e armadilhas
Podemos criar um slice a partir de outro slice usando a sintaxe s[inicio:fim]. O novo slice compartilha o mesmo array subjacente, a menos que ocorra realocação. Isso pode levar a comportamentos inesperados se não tomarmos cuidado.
Armadilha comum: modificar um elemento no sub-slice pode afetar o slice original (e vice-versa). Além disso, ao usar append em um sub-slice, se a capacidade do sub-slice não for suficiente, um novo array é criado, mas se for suficiente, o append pode sobrescrever elementos do slice original além do seu comprimento.
package main
import "fmt"
func main() {
original := []int{1, 2, 3, 4, 5}
sub := original[1:3] // sub = [2, 3], len=2, cap=4 (do índice 1 ao final)
fmt.Println("original:", original, "sub:", sub)
// Modificar sub afeta original
sub[0] = 20
fmt.Println("original após modificar sub:", original)
// Append em sub que não excede capacidade: sobrescreve elemento do original
sub = append(sub, 30) // sub agora [20, 3, 30], original fica [1, 20, 3, 30, 5]
fmt.Println("original após append em sub:", original)
fmt.Println("sub:", sub)
}
Para evitar essas armadilhas, podemos usar a função copy para criar um slice independente, ou usar append com cuidado. Entender o compartilhamento de memória é crucial para escrever código seguro.
Referências
- Go Blog: Arrays, slices (and strings): The mechanics of 'append'
- Go Specification: Slice types
- Go builtin: append
- Go by Example: Slices
- DigitalOcean: How to Use Slices in Go
Exercícios
-
Crie um slice de inteiros com valores 1, 2, 3, 4, 5. Use append para adicionar os valores 6, 7, 8. Imprima o slice resultante e sua capacidade.
✓ Resposta:package main import "fmt" func main() { s := []int{1, 2, 3, 4, 5} s = append(s, 6, 7, 8) fmt.Println("Slice:", s) fmt.Println("Capacidade:", cap(s)) } -
Dado o array
arr := [5]int{10, 20, 30, 40, 50}, crie um slice que contenha apenas os elementos do índice 1 ao 3 (exclusivo). Modifique o primeiro elemento do slice para 99 e imprima o array original. Explique o que aconteceu.✓ Resposta:package main import "fmt" func main() { arr := [5]int{10, 20, 30, 40, 50} slice := arr[1:3] // [20, 30] slice[0] = 99 fmt.Println("Array:", arr) // [10, 99, 30, 40, 50] }O slice compartilha o array subjacente, então modificar o slice altera o array original.
-
Escreva um programa que crie um slice com capacidade 5 e comprimento 3, preenchido com zeros. Em seguida, use append para adicionar mais 3 elementos. Imprima o comprimento e a capacidade após cada append.
✓ Resposta:package main import "fmt" func main() { s := make([]int, 3, 5) fmt.Printf("Início: len=%d cap=%d %v\n", len(s), cap(s), s) s = append(s, 1) fmt.Printf("Após 1: len=%d cap=%d %v\n", len(s), cap(s), s) s = append(s, 2) fmt.Printf("Após 2: len=%d cap=%d %v\n", len(s), cap(s), s) s = append(s, 3) fmt.Printf("Após 3: len=%d cap=%d %v\n", len(s), cap(s), s) }Saída esperada: len=3 cap=5, depois len=4 cap=5, len=5 cap=5, len=6 cap=10 (dobrou).
-
Explique por que o código a seguir pode causar um bug e como corrigi-lo:
s := []int{1, 2, 3, 4, 5} sub := s[0:2] sub = append(sub, 100) fmt.Println(s) // O que imprime?✓ Resposta:O código imprime
[1, 2, 100, 4, 5]. Oappendemsub(capacidade 5) sobrescreve o terceiro elemento do array original. Para evitar, usecopypara criar um slice independente:s := []int{1, 2, 3, 4, 5} sub := make([]int, 2) copy(sub, s[0:2]) sub = append(sub, 100) fmt.Println(s) // [1,2,3,4,5] -
Crie uma função que receba um slice de inteiros e retorne um novo slice contendo apenas os números pares. Use append para construir o resultado.
✓ Resposta:package main import "fmt" func pares(s []int) []int { var result []int for _, v := range s { if v%2 == 0 { result = append(result, v) } } return result } func main() { nums := []int{1, 2, 3, 4, 5, 6} fmt.Println("Pares:", pares(nums)) // [2,4,6] }
Boas práticas
Sempre atribua o resultado de append de volta à mesma variável (ou a uma nova). Evite manter referências a slices que compartilham o mesmo array subjacente se houver risco de mutação inesperada. Use make com capacidade adequada para evitar realocações frequentes. Prefira usar copy para criar slices independentes quando necessário.
Com esses conceitos, você está pronto para usar slices de forma eficiente e segura em Go.