Arrays: tamanho fixo e alocação na stack

1. Introdução aos Arrays em Go

Em Go, arrays são tipos de valor (value types) com tamanho fixo determinado em tempo de compilação. Diferentemente de slices, que são dinâmicos, arrays possuem uma quantidade imutável de elementos do mesmo tipo.

A sintaxe básica para declarar arrays:

var a [5]int                    // array de 5 inteiros, todos zero
b := [3]string{"a", "b", "c"}   // array de 3 strings
c := [5]int{1, 2, 3}            // [1 2 3 0 0] - elementos restantes são zero

A diferença fundamental entre arrays e slices é que arrays têm tamanho fixo e são value types, enquanto slices são descritores de uma fatia de um array subjacente, com tamanho dinâmico.

2. Tamanho Fixo: Parte do Tipo

Em Go, o tamanho é parte do tipo do array. [3]int e [4]int são tipos completamente diferentes. Isso tem implicações importantes para funções:

func somaTres(nums [3]int) int {
    total := 0
    for _, v := range nums {
        total += v
    }
    return total
}

func main() {
    a := [3]int{1, 2, 3}
    b := [4]int{1, 2, 3, 4}

    fmt.Println(somaTres(a)) // OK
    // fmt.Println(somaTres(b)) // Erro de compilação: cannot use b (type [4]int) as type [3]int
}

Quando você passa um array para uma função, ele é copiado por completo. Isso pode ser caro para arrays grandes:

func modificaArray(arr [3]int) {
    arr[0] = 999 // Modifica apenas a cópia local
}

func main() {
    original := [3]int{1, 2, 3}
    modificaArray(original)
    fmt.Println(original) // [1 2 3] - inalterado
}

Para contornar essa limitação, use slices ou ponteiros:

func modificaSlice(arr []int) {
    arr[0] = 999 // Modifica o array original
}

3. Alocação na Stack vs. Heap

Arrays pequenos (tipicamente < 64 KB) são alocados na stack, o que é extremamente rápido. O compilador Go usa escape analysis para decidir se um array pode ficar na stack ou precisa ir para o heap.

Exemplo: array alocado na stack

func criaArray() {
    a := [100]int{} // Alocado na stack (não escapa)
    for i := range a {
        a[i] = i
    }
}

Exemplo: array escapa para o heap

func criaArrayHeap() *[100]int {
    a := [100]int{} // Escapa para o heap porque retornamos um ponteiro
    for i := range a {
        a[i] = i
    }
    return &a
}

A diferença de desempenho é significativa. Alocação na stack é uma operação O(1) que apenas ajusta o ponteiro de stack, enquanto alocação no heap envolve gerenciamento de memória mais complexo e coleta de lixo.

Para verificar onde suas variáveis são alocadas, use a flag -gcflags="-m":

go build -gcflags="-m" seu_programa.go

4. Inicialização e Valores Zero

Arrays em Go são sempre inicializados. Se você não especificar valores, todos os elementos recebem o valor zero do tipo:

var a [5]int       // [0 0 0 0 0]
var b [3]string    // ["" "" ""]
var c [2]bool      // [false false]

Inicialização literal completa e parcial:

a := [5]int{1, 2, 3, 4, 5}    // Completa
b := [5]int{1, 2}             // Parcial: [1 2 0 0 0]
c := [5]int{4: 10}            // Apenas índice 4: [0 0 0 0 10]
d := [5]int{1, 4: 10}         // Misto: [1 0 0 0 10]

Uso de chave-valor para inicialização explícita:

tabela := [5]int{1: 100, 3: 300} // [0 100 0 300 0]
dias := [7]string{0: "Dom", 1: "Seg", 2: "Ter", 3: "Qua", 4: "Qui", 5: "Sex", 6: "Sáb"}

5. Operações com Arrays

Iteração:

arr := [3]string{"Go", "Python", "Rust"}

// Com range
for i, v := range arr {
    fmt.Printf("Índice %d: %s\n", i, v)
}

// Tradicional
for i := 0; i < len(arr); i++ {
    fmt.Println(arr[i])
}

Comparação direta:

Arrays do mesmo tipo podem ser comparados diretamente com == e !=:

a := [3]int{1, 2, 3}
b := [3]int{1, 2, 3}
c := [3]int{4, 5, 6}

fmt.Println(a == b) // true
fmt.Println(a == c) // false

Cópia explícita:

a := [3]int{1, 2, 3}
b := a  // Cópia completa, não referência
b[0] = 999
fmt.Println(a) // [1 2 3] - inalterado
fmt.Println(b) // [999 2 3]

Limitação importante: Não é possível usar append diretamente em arrays. Use slices para isso.

6. Arrays Multidimensionais

Arrays multidimensionais em Go são alocados de forma contígua na stack (para tamanhos pequenos):

var matriz [3][3]int
matriz[0] = [3]int{1, 2, 3}
matriz[1] = [3]int{4, 5, 6}
matriz[2] = [3]int{7, 8, 9}

Ou de forma mais concisa:

matriz := [3][3]int{
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9},
}

Iteração aninhada:

for i := 0; i < len(matriz); i++ {
    for j := 0; j < len(matriz[i]); j++ {
        fmt.Printf("%d ", matriz[i][j])
    }
    fmt.Println()
}

Diferença crucial: Arrays multidimensionais são blocos contíguos de memória. Já slices multidimensionais ([][]int) são slices de slices, com alocação não contígua e overhead adicional.

7. Casos de Uso e Boas Práticas

Quando usar arrays:

  • Buffers de tamanho fixo conhecido em tempo de compilação
  • Tabelas hash pequenas e fixas
  • Representação de matrizes em computação gráfica (ex: [4][4]float64 para matrizes 4x4)
  • Máxima performance em código crítico (evita alocações no heap)

Exemplo prático: buffer fixo para leitura

type Packet struct {
    Header [24]byte
    Payload [1024]byte
}

Quando preferir slices:

  • Tamanho desconhecido ou variável
  • Passagem para funções sem cópia excessiva
  • Operações de append e redimensionamento

Armadilhas comuns:

  1. Confundir array com slice: [3]int vs []int
  2. Cópia acidental em funções: Passar array grande por valor é caro
  3. Esquecer que tamanho é parte do tipo: [3]int não é compatível com [4]int

Dica: Use [...]int{1, 2, 3} para inferir o tamanho automaticamente:

arr := [...]int{1, 2, 3, 4, 5} // Equivalente a [5]int{1, 2, 3, 4, 5}
fmt.Printf("Tipo: %T, len: %d\n", arr, len(arr)) // Tipo: [5]int, len: 5

Referências