Slices
Nesta aula, exploramos slices em Go, estruturas dinâmicas que abstraem arrays. Aprendemos sobre len e cap, a função append, e como criar sub-slices, além de armadilhas comuns como compartilhamento de memória.
Slices são uma das estruturas de dados mais importantes e flexíveis em Go. Diferente de arrays, que têm tamanho fixo, slices são dinâmicos e podem crescer ou encolher conforme necessário. Nesta aula, vamos aprofundar o funcionamento dos slices, suas propriedades, operações e armadilhas comuns.
Um slice é uma abstração sobre um array subjacente. Ele fornece uma visão de uma parte (ou de todo) desse array, permitindo manipulação eficiente sem copiar dados. Internamente, um slice é composto por três campos: um ponteiro para o array, o comprimento (len) e a capacidade (cap).
Diferença de arrays
Arrays em Go têm tamanho fixo definido em tempo de compilação. Por exemplo, var a [5]int declara um array de 5 inteiros. Você não pode adicionar ou remover elementos; o tamanho é parte do tipo. Já slices não têm tamanho fixo; eles são dinâmicos e podem crescer. Um slice é declarado sem especificar o tamanho: var s []int.
Outra diferença importante é que arrays são valores: quando você atribui ou passa um array para uma função, ele é copiado inteiramente. Slices, por outro lado, são referências: eles apontam para um array subjacente. Atribuir um slice não copia os dados, apenas cria outra referência ao mesmo array.
package main
import "fmt"
func main() {
// Array: tamanho fixo
var arr [3]int = [3]int{1, 2, 3}
fmt.Println("Array:", arr)
// Slice: dinâmico
var slc []int = []int{1, 2, 3}
fmt.Println("Slice:", slc)
// Atribuição: array copia, slice referencia
arr2 := arr
arr2[0] = 100
fmt.Println("Array original:", arr) // [1 2 3]
slc2 := slc
slc2[0] = 100
fmt.Println("Slice original:", slc) // [100 2 3] (mesmo array subjacente)
}len e cap
Todo slice possui duas propriedades fundamentais: comprimento (len) e capacidade (cap). O comprimento é o número de elementos que o slice contém atualmente. A capacidade é o número máximo de elementos que o slice pode armazenar sem realocar o array subjacente. Esses valores podem ser obtidos com as funções nativas len() e cap().
A capacidade de um slice é determinada pelo array subjacente a partir da posição inicial do slice. Se você criar um slice a partir de um array, a capacidade será o número de elementos do início do slice até o final do array. Por exemplo, s := arr[1:3] tem comprimento 2 e capacidade igual a len(arr) - 1.
package main
import "fmt"
func main() {
arr := [5]int{10, 20, 30, 40, 50}
s := arr[1:4] // elementos 20, 30, 40
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) // len=3 cap=4
// Alterando o slice não afeta a capacidade inicial
s = s[:2] // reduz comprimento, capacidade permanece 4
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) // len=2 cap=4
}append
A função append é usada para adicionar elementos a um slice. Ela recebe o slice e os elementos a serem adicionados, e retorna um novo slice (ou o mesmo, se houver capacidade suficiente). Se a capacidade for insuficiente, append aloca um novo array subjacente com maior capacidade, copia os dados existentes e adiciona os novos elementos.
É importante atribuir o resultado de append de volta à variável do slice, pois o slice original pode ser inválido após a realocação. O crescimento da capacidade segue uma estratégia de duplicação para slices pequenos e um fator de crescimento menor para slices grandes.
package main
import "fmt"
func main() {
var s []int
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) // len=0 cap=0 []
s = append(s, 1)
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) // len=1 cap=1 [1]
s = append(s, 2, 3, 4)
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) // len=4 cap=4 [1 2 3 4]
s = append(s, 5)
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) // len=5 cap=8 [1 2 3 4 5]
}Slice de slice e armadilhas
Você pode criar slices a partir de outros slices, usando a mesma sintaxe de fatiamento. Por exemplo, sub := s[1:3] cria um novo slice que referencia o mesmo array subjacente do slice original. Isso significa que alterações em um slice podem afetar o outro, se eles compartilharem o mesmo array.
Uma armadilha comum ocorre quando você usa append em um sub-slice. Se a capacidade do sub-slice for suficiente, o append pode sobrescrever elementos do slice original. Por exemplo, se você tem s := []int{1,2,3,4} e cria sub := s[:2] (capacidade 4), fazer sub = append(sub, 100) modificará o terceiro elemento de s (de 3 para 100). Isso pode causar bugs sutis.
package main
import "fmt"
func main() {
s := []int{1, 2, 3, 4}
sub := s[:2] // sub = [1,2], cap=4
fmt.Println("Antes:", s, sub)
sub = append(sub, 100) // modifica s[2]
fmt.Println("Depois:", s, sub) // s = [1,2,100,4], sub = [1,2,100]
// Para evitar, use copy ou crie um slice com capacidade igual ao comprimento
sub2 := make([]int, 2)
copy(sub2, s[:2])
sub2 = append(sub2, 200)
fmt.Println("Com copy:", s, sub2) // s permanece [1,2,100,4]
}Boas práticas
Sempre atribua o resultado de append à mesma variável. Evite compartilhar slices entre funções sem entender o impacto na memória. Use copy para criar slices independentes quando necessário. Lembre-se de que slices são referências: se você precisar de uma cópia dos dados, use copy ou append com slice vazio.
Referências
- Go Slices: usage and internals
- Effective Go - Slices
- Go builtin append
- Go Specification - Slice types
- Go Slices: usage and internals (blog)
Exercícios
Crie um slice de inteiros com os valores 1, 2, 3, 4, 5. Use append para adicionar o valor 6. Imprima o slice.
✓ Resposta:package main import "fmt" func main() { s := []int{1, 2, 3, 4, 5} s = append(s, 6) fmt.Println(s) // [1 2 3 4 5 6] }Dado o array
arr := [5]int{10, 20, 30, 40, 50}, crie um slice que contenha os elementos do índice 1 ao 3 (exclusive 3). Imprima o comprimento e a capacidade do slice.✓ Resposta:package main import "fmt" func main() { arr := [5]int{10, 20, 30, 40, 50} s := arr[1:3] fmt.Printf("len=%d cap=%d\n", len(s), cap(s)) // len=2 cap=4 }Explique o que acontece quando você faz
appendem um slice que já atingiu sua capacidade. Como o Go lida com isso?✓ Resposta: Quando a capacidade é insuficiente,appendaloca um novo array subjacente com capacidade maior (geralmente dobrando para slices pequenos), copia os elementos existentes para o novo array e adiciona os novos. O slice retornado aponta para o novo array. O array antigo pode ser coletado pelo garbage collector se não houver mais referências.Crie um slice
s := []int{1, 2, 3, 4, 5}. Crie um sub-slicesub := s[1:4]. Modifique o elemento de índice 2 desubpara 99. Imprima ambos os slices. O que você observa?✓ Resposta:
Observação: Ambos os slices compartilham o mesmo array subjacente, então a modificação empackage main import "fmt" func main() { s := []int{1, 2, 3, 4, 5} sub := s[1:4] // [2,3,4] sub[2] = 99 // modifica o índice 2 de sub (que é o índice 3 de s) fmt.Println("s:", s) // [1 2 3 99 5] fmt.Println("sub:", sub) // [2 3 99] }subafetas.Escreva uma função que receba um slice de inteiros e retorne um novo slice contendo apenas os números pares. Use
append.✓ Resposta:package main import "fmt" func filterEven(s []int) []int { var result []int for _, v := range s { if v%2 == 0 { result = append(result, v) } } return result } func main() { s := []int{1, 2, 3, 4, 5, 6} fmt.Println(filterEven(s)) // [2 4 6] }