Arrays: A Estrutura Fixa de Dados
Arrays são coleções de tamanho fixo que armazenam elementos do mesmo tipo. Em Go, quando você declara um array, você está comprometendo-se com um tamanho específico que não pode ser alterado após a criação. Isso oferece previsibilidade de memória e performance, mas também limitações que você precisa compreender desde o início.
A sintaxe para declarar um array em Go é [tamanho]Tipo. O tamanho é parte da definição do tipo, o que significa que [5]int e [10]int são tipos completamente diferentes. Isso é uma diferença fundamental em relação a linguagens como Python ou JavaScript. Quando você cria um array, todos os elementos são inicializados com o valor zero do tipo (0 para números, "" para strings, false para booleans, etc.).
package main
import "fmt"
func main() {
// Declarando um array de 5 inteiros
var numeros [5]int
fmt.Println(numeros) // Output: [0 0 0 0 0]
// Inicializando com valores
idades := [3]int{25, 30, 35}
fmt.Println(idades) // Output: [25 30 35]
// Usando ... para inferir tamanho
nomes := [...]string{"Alice", "Bob", "Charlie"}
fmt.Println(len(nomes)) // Output: 3
// Acessando e modificando elementos
idades[1] = 31
fmt.Println(idades[1]) // Output: 31
// Iterando sobre um array
for i, valor := range idades {
fmt.Printf("Índice %d: %d\n", i, valor)
}
}
A principal vantagem dos arrays é a performance. Como o tamanho é conhecido em tempo de compilação, Go pode alocar a memória de forma contígua e eficiente. No entanto, essa rigidez torna os arrays inadequados para situações onde você não sabe antecipadamente quantos elementos precisará armazenar. Para esses casos, você usará slices, que é o tópico que faz a maioria dos iniciantes em Go finalmente entender quando usar qual estrutura.
Quando Usar Arrays
Use arrays quando o tamanho é conhecido e não muda. Exemplos práticos incluem: um baralho com 52 cartas, uma matriz de pixels em uma imagem fixa, ou configurações do sistema que nunca variam. Na prática industrial, você verá poucos arrays puros; a maioria das aplicações usa slices porque oferecem flexibilidade sem sacrificar performance significativa.
Slices: A Flexibilidade que Go Oferece
Slices são visões dinâmicas sobre um array subjacente. Eles não armazenam dados; apenas apontam para dados contidos em um array. Compreender essa distinção é crucial para dominar Go. Um slice consiste em três componentes internos: um ponteiro para o primeiro elemento, o comprimento (quantidade de elementos) e a capacidade (espaço disponível no array subjacente).
A sintaxe para declarar um slice é []Tipo — note a ausência do tamanho, que é justamente o que o diferencia de um array. Você pode criar um slice de várias formas: usando uma literal, cortando um array existente, ou usando a função make(). Cada abordagem tem seu propósito específico.
package main
import "fmt"
func main() {
// 1. Literal de slice
frutas := []string{"maçã", "banana", "laranja"}
fmt.Println(frutas) // Output: [maçã banana laranja]
fmt.Println(len(frutas)) // Output: 3
fmt.Println(cap(frutas)) // Output: 3
// 2. Criando com make (comprimento 0, capacidade 5)
numeros := make([]int, 0, 5)
fmt.Println(len(numeros)) // Output: 0
fmt.Println(cap(numeros)) // Output: 5
// 3. Cortando um array
array := [...]int{10, 20, 30, 40, 50}
slice := array[1:4] // Índices 1, 2, 3
fmt.Println(slice) // Output: [20 30 40]
fmt.Println(len(slice)) // Output: 3
fmt.Println(cap(slice)) // Output: 4 (capacidade é 5 - 1)
// 4. Adicionando elementos com append
numeros = append(numeros, 100)
fmt.Println(numeros) // Output: [100]
fmt.Println(cap(numeros)) // Output: 5
// 5. Slice com make e comprimento inicial
letras := make([]string, 3, 5)
letras[0] = "a"
letras[1] = "b"
letras[2] = "c"
fmt.Println(letras) // Output: [a b c]
}
A função append() é onde a dinâmica dos slices se manifesta. Quando você adiciona elementos a um slice que não tem capacidade, Go automaticamente cria um novo array subjacente maior (geralmente dobrando a capacidade) e copia os dados existentes. Isso é transparente para você, mas compreender esse mecanismo explica por que algumas operações de append são mais caras que outras. Se você sabe antecipadamente quantos elementos precisará, criar um slice com make([]T, 0, capacidade) evita realocações desnecessárias.
Cortando Slices
O operador de corte [inicio:fim] cria um novo slice que compartilha o mesmo array subjacente. Modificações no novo slice afetam o slice original se referenciarem os mesmos elementos. Esse comportamento pode ser perigoso se não for compreendido, mas é extremamente poderoso quando bem utilizado.
package main
import "fmt"
func main() {
numeros := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// Cortando do índice 2 até 7 (não inclui 7)
slice1 := numeros[2:7]
fmt.Println(slice1) // Output: [3 4 5 6 7]
// Modificando o slice1 modifica o array original
slice1[0] = 999
fmt.Println(numeros) // Output: [1 2 999 4 5 6 7 8 9 10]
// Corte omitindo o início
slice2 := numeros[:3]
fmt.Println(slice2) // Output: [1 2 999]
// Corte omitindo o fim
slice3 := numeros[6:]
fmt.Println(slice3) // Output: [7 8 9 10]
// Corte com capacidade
slice4 := numeros[2:5:8] // inicio:fim:capacidade
fmt.Println(slice4) // Output: [999 4 5]
fmt.Println(cap(slice4)) // Output: 6 (8 - 2)
}
Maps: Associações Chave-Valor
Maps são estruturas de dados que associam chaves a valores. Diferentemente de arrays e slices (indexados por posição), maps usam chaves arbitrárias. Em Go, a sintaxe é map[TipoChave]TipoValor. As chaves devem ser de um tipo que suporte comparação de igualdade (números, strings, booleanos, etc.), enquanto os valores podem ser de qualquer tipo.
Um aspecto crucial: maps em Go são não ordenados. Cada vez que você itera sobre um map, a ordem dos elementos é aleatória. Isso é uma decisão deliberada do design da linguagem e diferencia Go de algumas outras linguagens. Se você precisa de uma ordem garantida, deve usar outras estruturas ou manter uma slice de chaves ordenadas separadamente.
package main
import "fmt"
func main() {
// 1. Declarando e inicializando um map
pessoa := map[string]string{
"nome": "Alice",
"cidade": "São Paulo",
"país": "Brasil",
}
fmt.Println(pessoa) // Output: map[cidade:São Paulo país:Brasil nome:Alice]
// 2. Criando um map vazio com make
contagem := make(map[string]int)
contagem["maçã"] = 5
contagem["banana"] = 3
contagem["laranja"] = 8
fmt.Println(contagem) // Output: map[laranja:8 maçã:5 banana:3]
// 3. Acessando valores
fmt.Println(pessoa["nome"]) // Output: Alice
// 4. Verificando se uma chave existe
valor, existe := pessoa["idade"]
fmt.Println(valor, existe) // Output: false
valor, existe = pessoa["nome"]
fmt.Println(valor, existe) // Output: Alice true
// 5. Iterando sobre um map
for chave, valor := range pessoa {
fmt.Printf("%s: %s\n", chave, valor)
}
// 6. Deletando uma chave
delete(pessoa, "país")
fmt.Println(pessoa) // Output: map[cidade:São Paulo nome:Alice]
// 7. Comprimento de um map
fmt.Println(len(pessoa)) // Output: 2
}
Maps são particularmente úteis para cachês, contadores, índices rápidos e associações dinâmicas. A busca por chave em um map é O(1) em média, tornando-os ideais para quando você precisa de acesso rápido por algum identificador. A limitação é que não há ordenamento garantido e eles usam mais memória que slices equivalentes.
Maps de Tipos Complexos
Você pode ter maps de praticamente qualquer coisa. Um padrão comum é maps de slices para construir índices invertidos, ou maps de structs para representar entidades complexas. Isso oferece uma flexibilidade poderosa, mas exige disciplina para não criar estruturas caóticas.
package main
import "fmt"
func main() {
// Map de strings para slices de inteiros
grupos := map[string][]int{
"pares": {2, 4, 6, 8},
"ímpares": {1, 3, 5, 7},
"zeros": {0},
}
for chave, valores := range grupos {
fmt.Printf("%s: %v\n", chave, valores)
}
// Adicionando elementos a uma slice dentro do map
grupos["pares"] = append(grupos["pares"], 10)
fmt.Println(grupos["pares"]) // Output: [2 4 6 8 10]
// Map de strings para structs
type Usuario struct {
ID int
Email string
}
usuarios := map[string]Usuario{
"alice": {ID: 1, Email: "alice@example.com"},
"bob": {ID: 2, Email: "bob@example.com"},
}
fmt.Println(usuarios["alice"].Email) // Output: alice@example.com
}
Padrões Práticos e Boas Práticas
Agora que você compreende os três pilares das coleções em Go, é essencial saber quando aplicá-los. Arrays são raros em código de produção; você os verá em situações muito específicas como buffers fixos ou implementações de estruturas de baixo nível. Slices são as rainhas das coleções em Go — use-as como padrão quando precisar de coleções dinâmicas. Maps devem ser usados para associações lógicas, não como arrays associativos genéricos.
Um padrão importante é o uso de slices de structs para representar coleções de objetos relacionados. Por exemplo, uma lista de usuários ou transações. Esse padrão combina a eficiência de slices com a estrutura de dados que structs oferecem. Outro padrão comum é usar maps para caches ou índices rápidos sobre dados já armazenados em slices.
package main
import "fmt"
func main() {
// Padrão: Slice de structs com índice em map
type Produto struct {
ID int
Nome string
Preço float64
}
// Armazenamento principal
produtos := []Produto{
{ID: 1, Nome: "Laptop", Preço: 2500.00},
{ID: 2, Nome: "Mouse", Preço: 50.00},
{ID: 3, Nome: "Teclado", Preço: 150.00},
}
// Índice para busca rápida
indice := make(map[int]int) // ID -> índice no slice
for i, p := range produtos {
indice[p.ID] = i
}
// Buscando um produto
id := 2
if idx, existe := indice[id]; existe {
fmt.Println(produtos[idx]) // Output: {2 Mouse 50}
}
// Filtrando produtos por critério
func() {
var produtosCaros []Produto
for _, p := range produtos {
if p.Preço > 100 {
produtosCaros = append(produtosCaros, p)
}
}
fmt.Println(produtosCaros)
}()
// Contadores com map
nomes := []string{"Alice", "Bob", "Alice", "Charlie", "Bob", "Alice"}
frequencia := make(map[string]int)
for _, nome := range nomes {
frequencia[nome]++
}
fmt.Println(frequencia) // Output: map[Alice:3 Bob:2 Charlie:1]
}
Uma consideração importante é a cópia de dados. Slices e maps são cópias superficiais — quando você atribui um slice a uma variável, você está copiando o cabeçalho (ponteiro, comprimento, capacidade), não os dados. Isso torna operações de cópia eficientes, mas você precisa estar ciente de que modificações afetam o original. Para uma cópia profunda de um slice, use copy() ou append() com inicialização.
package main
import "fmt"
func main() {
// Cópia superficial vs profunda
original := []int{1, 2, 3, 4, 5}
// Superficial - compartilha dados
superficial := original
superficial[0] = 999
fmt.Println(original) // Output: [999 2 3 4 5]
// Profunda com make
original2 := []int{1, 2, 3, 4, 5}
profunda := make([]int, len(original2))
copy(profunda, original2)
profunda[0] = 888
fmt.Println(original2) // Output: [1 2 3 4 5]
fmt.Println(profunda) // Output: [888 2 3 4 5]
}