Go

Dominando Arrays, Slices e Maps em Go: A Base das Coleções em Projetos Reais

12 min de leitura

Dominando Arrays, Slices e Maps em Go: A Base das Coleções em Projetos Reais

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 é . O tamanho é parte da definição do tipo, o que significa que e 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.). 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

<h2>Arrays: A Estrutura Fixa de Dados</h2>

<p>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.</p>

<p>A sintaxe para declarar um array em Go é <code>[tamanho]Tipo</code>. O tamanho é parte da definição do tipo, o que significa que <code>[5]int</code> e <code>[10]int</code> 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, &quot;&quot; para strings, false para booleans, etc.).</p>

<pre><code class="language-go">package main

import &quot;fmt&quot;

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{&quot;Alice&quot;, &quot;Bob&quot;, &quot;Charlie&quot;}

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(&quot;Índice %d: %d\n&quot;, i, valor)

}

}</code></pre>

<p>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á <strong>slices</strong>, que é o tópico que faz a maioria dos iniciantes em Go finalmente entender quando usar qual estrutura.</p>

<h3>Quando Usar Arrays</h3>

<p>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.</p>

<h2>Slices: A Flexibilidade que Go Oferece</h2>

<p>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).</p>

<p>A sintaxe para declarar um slice é <code>[]Tipo</code> — 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 <code>make()</code>. Cada abordagem tem seu propósito específico.</p>

<pre><code class="language-go">package main

import &quot;fmt&quot;

func main() {

// 1. Literal de slice

frutas := []string{&quot;maçã&quot;, &quot;banana&quot;, &quot;laranja&quot;}

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] = &quot;a&quot;

letras[1] = &quot;b&quot;

letras[2] = &quot;c&quot;

fmt.Println(letras) // Output: [a b c]

}</code></pre>

<p>A função <code>append()</code> é 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 <code>make([]T, 0, capacidade)</code> evita realocações desnecessárias.</p>

<h3>Cortando Slices</h3>

<p>O operador de corte <code>[inicio:fim]</code> 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.</p>

<pre><code class="language-go">package main

import &quot;fmt&quot;

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)

}</code></pre>

<h2>Maps: Associações Chave-Valor</h2>

<p>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 é <code>map[TipoChave]TipoValor</code>. 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.</p>

<p>Um aspecto crucial: maps em Go são <strong>não ordenados</strong>. 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.</p>

<pre><code class="language-go">package main

import &quot;fmt&quot;

func main() {

// 1. Declarando e inicializando um map

pessoa := map[string]string{

&quot;nome&quot;: &quot;Alice&quot;,

&quot;cidade&quot;: &quot;São Paulo&quot;,

&quot;país&quot;: &quot;Brasil&quot;,

}

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[&quot;maçã&quot;] = 5

contagem[&quot;banana&quot;] = 3

contagem[&quot;laranja&quot;] = 8

fmt.Println(contagem) // Output: map[laranja:8 maçã:5 banana:3]

// 3. Acessando valores

fmt.Println(pessoa[&quot;nome&quot;]) // Output: Alice

// 4. Verificando se uma chave existe

valor, existe := pessoa[&quot;idade&quot;]

fmt.Println(valor, existe) // Output: false

valor, existe = pessoa[&quot;nome&quot;]

fmt.Println(valor, existe) // Output: Alice true

// 5. Iterando sobre um map

for chave, valor := range pessoa {

fmt.Printf(&quot;%s: %s\n&quot;, chave, valor)

}

// 6. Deletando uma chave

delete(pessoa, &quot;país&quot;)

fmt.Println(pessoa) // Output: map[cidade:São Paulo nome:Alice]

// 7. Comprimento de um map

fmt.Println(len(pessoa)) // Output: 2

}</code></pre>

<p>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.</p>

<h3>Maps de Tipos Complexos</h3>

<p>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.</p>

<pre><code class="language-go">package main

import &quot;fmt&quot;

func main() {

// Map de strings para slices de inteiros

grupos := map[string][]int{

&quot;pares&quot;: {2, 4, 6, 8},

&quot;ímpares&quot;: {1, 3, 5, 7},

&quot;zeros&quot;: {0},

}

for chave, valores := range grupos {

fmt.Printf(&quot;%s: %v\n&quot;, chave, valores)

}

// Adicionando elementos a uma slice dentro do map

grupos[&quot;pares&quot;] = append(grupos[&quot;pares&quot;], 10)

fmt.Println(grupos[&quot;pares&quot;]) // Output: [2 4 6 8 10]

// Map de strings para structs

type Usuario struct {

ID int

Email string

}

usuarios := map[string]Usuario{

&quot;alice&quot;: {ID: 1, Email: &quot;alice@example.com&quot;},

&quot;bob&quot;: {ID: 2, Email: &quot;bob@example.com&quot;},

}

fmt.Println(usuarios[&quot;alice&quot;].Email) // Output: alice@example.com

}</code></pre>

<h2>Padrões Práticos e Boas Práticas</h2>

<p>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.</p>

<p>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.</p>

<pre><code class="language-go">package main

import &quot;fmt&quot;

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: &quot;Laptop&quot;, Preço: 2500.00},

{ID: 2, Nome: &quot;Mouse&quot;, Preço: 50.00},

{ID: 3, Nome: &quot;Teclado&quot;, Preço: 150.00},

}

// Índice para busca rápida

indice := make(map[int]int) // ID -&gt; í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 &gt; 100 {

produtosCaros = append(produtosCaros, p)

}

}

fmt.Println(produtosCaros)

}()

// Contadores com map

nomes := []string{&quot;Alice&quot;, &quot;Bob&quot;, &quot;Alice&quot;, &quot;Charlie&quot;, &quot;Bob&quot;, &quot;Alice&quot;}

frequencia := make(map[string]int)

for _, nome := range nomes {

frequencia[nome]++

}

fmt.Println(frequencia) // Output: map[Alice:3 Bob:2 Charlie:1]

}</code></pre>

<p>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 <code>copy()</code> ou <code>append()</code> com inicialização.</p>

<pre><code class="language-go">package main

import &quot;fmt&quot;

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]

}</code></pre>

<h2>Referências</h2>

<ul>

<li><a href="https://go.dev/ref/spec#Array_types" target="_blank" rel="noopener noreferrer">Go Spec - Arrays</a></li>

<li><a href="https://go.dev/ref/spec#Slice_types" target="_blank" rel="noopener noreferrer">Go Spec - Slices</a></li>

<li><a href="https://go.dev/ref/spec#Map_types" target="_blank" rel="noopener noreferrer">Go Spec - Maps</a></li>

<li><a href="https://go.dev/doc/effective_go#arrays" target="_blank" rel="noopener noreferrer">Effective Go - Arrays, slices, and maps</a></li>

<li><a href="https://go.dev/blog/slices-intro" target="_blank" rel="noopener noreferrer">The Go Blog - Slices: usage and internals</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Go

Select em Go: Multiplexando Channels e Timeouts: Do Básico ao Avançado
Select em Go: Multiplexando Channels e Timeouts: Do Básico ao Avançado

Introdução ao Select em Go O é uma das construções mais poderosas da linguage...

Como Usar Benchmarks e Profiling em Go: testing.B e pprof na Prática em Produção
Como Usar Benchmarks e Profiling em Go: testing.B e pprof na Prática em Produção

Introdução aos Benchmarks em Go Quando desenvolvemos software em Go, é comum...

Guia Completo de Pacote time em Go: Datas, Durações, Timers e Tickers
Guia Completo de Pacote time em Go: Datas, Durações, Timers e Tickers

Introdução ao Pacote time em Go O pacote é um dos pilares fundamentais da pro...