O que é Slice em Go?
Um slice é a estrutura de dados mais utilizada em Go para trabalhar com coleções de elementos do mesmo tipo. Diferente de um array, que tem tamanho fixo definido em tempo de compilação, um slice é uma referência flexível a uma porção de um array subjacente, permitindo crescimento e redução dinâmicos durante a execução do programa.
Na prática, quando você precisa de uma lista de itens em Go — seja uma lista de usuários, resultados de uma query no banco de dados ou bytes lidos de um arquivo — você vai usar slices em quase 100% dos casos. Arrays puros são raramente usados diretamente no código Go idiomático.
Internamente, um slice é composto por três campos:
- Pointer: ponteiro para o primeiro elemento do array subjacente acessível pelo slice
- Length (len): número de elementos que o slice contém atualmente
- Capacity (cap): número total de elementos no array subjacente a partir do ponteiro
Essa estrutura interna é fundamental para entender o comportamento de slices, especialmente em cenários de compartilhamento de dados e crescimento dinâmico.
Declarando e inicializando slices
Existem várias formas de criar um slice em Go:
Literal de slice
// Slice de inteiros
numeros := []int{1, 2, 3, 4, 5}
// Slice de strings
linguagens := []string{"Go", "Python", "Rust"}
// Slice vazio (não nil)
vazio := []int{}
Usando make()
A função make() permite criar slices com length e capacity pré-definidos:
// Slice com length 5 e capacity 5
s1 := make([]int, 5)
// Slice com length 0 e capacity 10
s2 := make([]int, 0, 10)
Usar make() com capacity pré-alocada é uma otimização importante quando você sabe antecipadamente quantos elementos vai inserir, pois evita realocações durante o crescimento do slice.
A partir de um array
array := [5]int{10, 20, 30, 40, 50}
// Slice referenciando os elementos do índice 1 ao 3 (exclusive)
s := array[1:4] // [20, 30, 40]
Operações fundamentais com slices
append() — Adicionando elementos
A função append() é a forma padrão de adicionar elementos a um slice:
frutas := []string{"maçã", "banana"}
frutas = append(frutas, "laranja")
frutas = append(frutas, "uva", "manga") // múltiplos elementos
// Concatenando dois slices
mais := []string{"pêssego", "abacaxi"}
frutas = append(frutas, mais...)
Quando o append() precisa de mais espaço do que a capacity atual permite, Go aloca um novo array subjacente com capacity maior (geralmente dobra até 256 elementos, depois cresce em ~25%). O slice antigo continua apontando para o array original até ser coletado pelo garbage collector.
len() e cap()
s := make([]int, 3, 10)
fmt.Println(len(s)) // 3
fmt.Println(cap(s)) // 10
Slicing (fatiamento)
original := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// Sintaxe: slice[low:high]
parte := original[2:5] // [2, 3, 4]
// Omitindo índices
inicio := original[:3] // [0, 1, 2]
fim := original[7:] // [7, 8, 9]
tudo := original[:] // cópia da referência completa
// Sintaxe de três índices (limita capacity)
limitado := original[2:5:5] // len=3, cap=3
A sintaxe de três índices ([low:high:max]) é especialmente útil para evitar o problema do backing array compartilhado.
copy() — Copiando slices com segurança
origem := []int{1, 2, 3, 4, 5}
destino := make([]int, len(origem))
n := copy(destino, origem) // n = 5 (elementos copiados)
A função copy() cria uma cópia independente dos dados, quebrando qualquer relação com o array subjacente original. Use-a quando precisar modificar um slice sem afetar outros que compartilham o mesmo backing array.
Slice vs Array: diferenças fundamentais
| Característica | Array | Slice |
|---|---|---|
| Tamanho | Fixo em compilação | Dinâmico |
| Declaração | [5]int | []int |
| Passagem por função | Cópia completa (valor) | Referência (header) |
Comparação com == | Suportada | Apenas com nil |
| Uso idiomático | Raro | Onipresente |
Arrays são tipos por valor — ao passar um array para uma função, Go copia todos os elementos. Slices passam apenas o header de 24 bytes (ponteiro + length + capacity), tornando-os muito mais eficientes para coleções grandes.
nil slice vs slice vazio
Essa distinção é uma fonte comum de confusão:
var nilSlice []int // nil slice: pointer=nil, len=0, cap=0
vazioSlice := []int{} // empty slice: pointer!=nil, len=0, cap=0
fmt.Println(nilSlice == nil) // true
fmt.Println(vazioSlice == nil) // false
// Ambos funcionam com append, len, cap e range
fmt.Println(len(nilSlice)) // 0
nilSlice = append(nilSlice, 1) // funciona perfeitamente
Na prática, para a maioria das operações, ambos se comportam igualmente. Porém, na serialização JSON, um nil slice gera null enquanto um slice vazio gera [] — algo relevante ao construir APIs REST.
Armadilhas comuns com slices
Backing array compartilhado
O maior gotcha de slices em Go é o compartilhamento do array subjacente:
original := []int{1, 2, 3, 4, 5}
fatia := original[1:3] // [2, 3]
fatia[0] = 99 // Modifica o original também!
fmt.Println(original) // [1, 99, 3, 4, 5]
Para evitar isso, use copy() ou a sintaxe de três índices para limitar a capacity.
Vazamento de memória
Manter um slice pequeno que referencia um array grande impede o garbage collector de liberar o array inteiro:
// Ruim: mantém referência ao array grande
func primeiroByte(dados []byte) []byte {
return dados[:1]
}
// Bom: copia para liberar o array original
func primeiroByte(dados []byte) []byte {
resultado := make([]byte, 1)
copy(resultado, dados[:1])
return resultado
}
Esse padrão é especialmente importante ao trabalhar com leitura de arquivos ou respostas HTTP grandes.
Iterando sobre slices
O range é a forma idiomática de iterar:
nomes := []string{"Ana", "Bruno", "Carla"}
// Índice e valor
for i, nome := range nomes {
fmt.Printf("%d: %s\n", i, nome)
}
// Apenas valor
for _, nome := range nomes {
fmt.Println(nome)
}
// Apenas índice
for i := range nomes {
fmt.Println(i)
}
A partir do Go 1.22+, é possível usar iteradores customizados com range over func para padrões mais avançados de iteração.
Slices e concorrência
Slices não são seguros para uso concorrente sem sincronização. Se múltiplas goroutines precisam acessar o mesmo slice, use:
- Mutex para proteção de acesso
- Channels para comunicação entre goroutines
sync.RWMutexpara cenários com múltiplos leitores
var mu sync.Mutex
var dados []int
func adicionarSeguro(valor int) {
mu.Lock()
defer func() { mu.Unlock() }()
dados = append(dados, valor)
}
Boas práticas com slices
- Pré-aloque capacity com
make()quando souber o tamanho aproximado - Use copy() quando precisar de independência do array original
- Prefira nil slices a slices vazios como valor zero
- Cuidado com append em slices fatiados — pode modificar dados compartilhados
- Limpe referências em slices de ponteiros para evitar memory leaks
Para aprender mais sobre estruturas de dados em Go, explore o glossário de structs e os tutoriais de Go para iniciantes.
Perguntas frequentes (FAQ)
Qual a diferença entre slice e array em Go?
Um array tem tamanho fixo definido em compilação ([5]int), enquanto um slice ([]int) é dinâmico e pode crescer com append(). Na prática, slices são usados em 99% dos casos por serem mais flexíveis e eficientes na passagem para funções — apenas o header de 24 bytes é copiado, não os dados inteiros.
Quando usar make() para criar um slice?
Use make() quando souber antecipadamente a quantidade de elementos que o slice vai conter. Isso evita realocações durante append(), melhorando a performance da aplicação. A forma make([]T, 0, capacidade) cria um slice vazio pronto para receber até capacidade elementos sem realocação.
Como evitar bugs com backing array compartilhado?
Use copy() para criar uma cópia independente dos dados ou a sintaxe de três índices slice[low:high:max] para limitar a capacity. Isso evita que modificações em um slice afetem outros que referenciam o mesmo array subjacente — um dos bugs mais sutis em Go.
Nil slice ou slice vazio — qual usar?
Prefira nil slices (var s []int) como valor zero padrão. Ambos funcionam identicamente com append, len, cap e range. A diferença aparece na serialização: nil gera null em JSON e slice vazio gera []. Se sua API REST precisa retornar um array JSON vazio, use []int{}.