Golang

Arrays e Slices: a espinha dorsal das coleções em Go Já leu

8 min de leitura

Arrays e Slices: a espinha dorsal das coleções em Go
Toda linguagem de programação precisa de uma forma de armazenar sequências de valores. Go oferece duas estruturas para isso: arrays e s

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

Comentários

Mais em Golang

Ponteiros: conceito, uso e quando evitar
Ponteiros: conceito, uso e quando evitar

Ponteiros s&atilde;o um dos t&oacute;picos que mais intimidam iniciantes vind...

Interfaces: contratos implícitos e polimorfismo
Interfaces: contratos implícitos e polimorfismo

&nbsp; Interfaces existem em Java, C#, TypeScript e diversas outras linguage...

Introdução ao Go: história, filosofia e os criadores da linguagem
Introdução ao Go: história, filosofia e os criadores da linguagem

Talvez voc&ecirc; se pergunte:&nbsp;Por que mais uma linguagem de programa&cc...