Toda linguagem de programação precisa de uma forma de armazenar sequências de valores. Go oferece duas estruturas para isso: arrays e slices. À primeira vista parecem similares, mas têm naturezas fundamentalmente diferentes. Arrays são rígidos e raramente usados diretamente. Slices são flexíveis, poderosos e estão em praticamente todo código Go real. Entender a diferença entre os dois — e como um é construído sobre o outro — é essencial para escrever Go idiomático.
Arrays: tamanho fixo em tempo de compilação
Um array em Go tem tamanho fixo, definido na declaração e imutável para sempre. O tamanho faz parte do tipo — [3]int e [5]int são tipos completamente diferentes e incompatíveis:
package main
import "fmt"
func main() {
var notas [5]float64
notas[0] = 7.5
notas[1] = 8.0
notas[2] = 9.5
notas[3] = 6.0
notas[4] = 8.5
fmt.Println(notas) // [7.5 8 9.5 6 8.5]
fmt.Println(len(notas)) // 5
}
É possível declarar e inicializar em uma única linha com um literal de array:
primos := [5]int{2, 3, 5, 7, 11}
Ou deixar o compilador contar os elementos automaticamente com ...:
primos := [...]int{2, 3, 5, 7, 11}
fmt.Println(len(primos)) // 5
Arrays em Go são passados por valor. Quando um array é atribuído a outra variável ou passado para uma função, uma cópia completa de todos os elementos é feita. Para um array de um milhão de inteiros, isso significa copiar um milhão de inteiros. Esse comportamento é uma das razões pelas quais arrays diretos são pouco usados em Go — slices resolvem esse problema com elegância.
Slices: a visão dinâmica sobre um array
Um slice é uma estrutura leve que descreve uma sequência de elementos de um array subjacente. Internamente, um slice é composto por três campos:
- Um ponteiro para o primeiro elemento do array que o slice enxerga
- Um comprimento (
len) — quantos elementos o slice contém atualmente - Uma capacidade (
cap) — quantos elementos existem no array subjacente a partir do ponteiro
Essa estrutura interna tem tamanho fixo e pequeno, independentemente de quantos elementos o slice referencia. Por isso, slices são passados por valor de forma eficiente — a cópia é apenas do cabeçalho de três campos, não dos dados.
Criando slices
A partir de um literal:
linguagens := []string{"Go", "Rust", "Python", "Java"}
fmt.Println(linguagens) // [Go Rust Python Java]
fmt.Println(len(linguagens)) // 4
fmt.Println(cap(linguagens)) // 4
Note a ausência de tamanho entre os colchetes — isso distingue um slice []string de um array [4]string.
Com a função make:
make cria um slice com comprimento e capacidade definidos, todos os elementos inicializados com o valor zero do tipo:
numeros := make([]int, 5) // len=5, cap=5
fmt.Println(numeros) // [0 0 0 0 0]
buffer := make([]byte, 0, 100) // len=0, cap=100
fmt.Println(len(buffer), cap(buffer)) // 0 100
A distinção entre len e cap em make é importante: len define quantos elementos estão disponíveis para uso imediato, enquanto cap reserva memória para crescimento futuro sem realocação.
Fatiamento: slices de slices
A operação de fatiamento cria um novo slice que aponta para uma região do array subjacente. A sintaxe é s[baixo:alto], onde baixo é inclusivo e alto é exclusivo:
numeros := []int{10, 20, 30, 40, 50}
a := numeros[1:4] // [20 30 40]
b := numeros[:3] // [10 20 30] — baixo omitido assume 0
c := numeros[2:] // [30 40 50] — alto omitido assume len
d := numeros[:] // [10 20 30 40 50] — cópia do cabeçalho
fmt.Println(a, b, c, d)
Atenção crítica: slices criados por fatiamento compartilham o array subjacente. Modificar elementos de um slice modifica os dados visíveis pelo outro:
original := []int{1, 2, 3, 4, 5}
fatia := original[1:4] // [2 3 4]
fatia[0] = 99
fmt.Println(original) // [1 99 3 4 5] — original foi afetado!
fmt.Println(fatia) // [99 3 4]
Para obter um slice verdadeiramente independente, use copy:
original := []int{1, 2, 3, 4, 5}
independente := make([]int, len(original))
copy(independente, original)
independente[0] = 99
fmt.Println(original) // [1 2 3 4 5] — inalterado
fmt.Println(independente) // [99 2 3 4 5]
Append: adicionando elementos
A função embutida append adiciona elementos ao final de um slice e retorna o slice resultante. Sempre atribua o retorno de append — o slice original não é modificado diretamente:
s := []int{1, 2, 3}
s = append(s, 4)
s = append(s, 5, 6, 7)
fmt.Println(s) // [1 2 3 4 5 6 7]
Para concatenar dois slices, use o operador ... para expandir o segundo:
a := []int{1, 2, 3}
b := []int{4, 5, 6}
c := append(a, b...)
fmt.Println(c) // [1 2 3 4 5 6]
Como o append gerencia memória
Quando append precisa adicionar elementos além da capacidade atual do slice, Go aloca um novo array subjacente com capacidade maior, copia os dados existentes e retorna um slice apontando para o novo array. O array antigo é descartado pelo garbage collector se não houver mais referências a ele.
A estratégia de crescimento dobra a capacidade para slices pequenos e cresce de forma mais conservadora para slices maiores. O ponto importante é que após um append que causou realocação, o novo slice não compartilha mais o array com o original:
original := make([]int, 3, 3) // len=3, cap=3
original[0], original[1], original[2] = 1, 2, 3
expandido := append(original, 4) // causa realocação
expandido[0] = 99
fmt.Println(original) // [1 2 3] — não afetado
fmt.Println(expandido) // [99 2 3 4]
Para evitar realocações frequentes quando o tamanho final é conhecido, pré-aloque a capacidade com make:
// Ineficiente: múltiplas realocações
s := []int{}
for i := 0; i < 10000; i++ {
s = append(s, i)
}
// Eficiente: nenhuma realocação
s := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
s = append(s, i)
}
Iterando sobre slices
A forma idiomática de iterar é com for range:
frutas := []string{"maçã", "banana", "laranja", "uva"}
for i, fruta := range frutas {
fmt.Printf("%d: %s\n", i, fruta)
}
Quando apenas o valor importa:
for _, fruta := range frutas {
fmt.Println(fruta)
}
Quando apenas o índice importa:
for i := range frutas {
fmt.Println(i)
}
Removendo elementos de um slice
Go não possui uma função embutida de remoção. O padrão idiomático usa fatiamento e append:
s := []int{10, 20, 30, 40, 50}
i := 2 // índice do elemento a remover (30)
s = append(s[:i], s[i+1:]...)
fmt.Println(s) // [10 20 40 50]
Essa operação preserva a ordem dos elementos. Se a ordem não importa, trocar o elemento a remover pelo último e reduzir o comprimento é mais eficiente por evitar o deslocamento de elementos:
s[i] = s[len(s)-1]
s = s[:len(s)-1]
fmt.Println(s) // [10 20 50 40] — ordem não preservada, mas mais rápido
Slices multidimensionais
Go não tem arrays bidimensionais nativos no estilo de outras linguagens, mas slices de slices cumprem o papel:
matriz := [][]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}
for _, linha := range matriz {
for _, val := range linha {
fmt.Printf("%d ", val)
}
fmt.Println()
}
Slices de structs: o caso mais comum na prática
Em código Go real, slices de structs são onipresentes — listas de usuários, produtos, registros de banco de dados, resultados de API:
package main
import "fmt"
type Produto struct {
Nome string
Preco float64
}
func main() {
produtos := []Produto{
{"Teclado", 250.00},
{"Mouse", 120.00},
{"Monitor", 1800.00},
}
total := 0.0
for _, p := range produtos {
total += p.Preco
fmt.Printf("%-10s R$ %.2f\n", p.Nome, p.Preco)
}
fmt.Printf("%-10s R$ %.2f\n", "Total", total)
}
Resumo do que foi coberto
Este artigo apresentou arrays e slices em profundidade. Arrays foram introduzidos como estruturas de tamanho fixo passadas por valor. Slices foram explicados a partir de sua estrutura interna de três campos, cobrindo criação com literais e make, fatiamento, compartilhamento de array subjacente, copy para independência, append com sua lógica de realocação, remoção de elementos e slices multidimensionais. Com esse conhecimento, o próximo artigo pode avançar para maps — outra estrutura fundamental da linguagem.
Referências e leituras complementares
-
Go Blog: Go Slices — usage and internals — Artigo oficial e definitivo sobre como slices funcionam internamente. https://go.dev/blog/slices-intro
-
Go Blog: Arrays, slices and strings — the mechanics of append — Aprofundamento técnico sobre append e realocação. https://go.dev/blog/slices
-
A Tour of Go — More types — Seção interativa sobre arrays, slices e maps. https://go.dev/tour/moretypes/6
-
Go by Example: Slices — Exemplos práticos e comentados de operações com slices. https://gobyexample.com/slices
-
Especificação da linguagem Go — Slice types — Definição formal do tipo slice. https://go.dev/ref/spec#Slice_types
-
Documentação de funções embutidas: append, copy, make — Referência das funções embutidas para slices. https://pkg.go.dev/builtin