Go Admin

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

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

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]
}

Referências


Artigos relacionados