Go: Matrizes e fatias em Go

As fatias são o cerne da forma como o Go lida com dados de coleções — elas são “visualizações” leves de matrizes, e sua capacidade de redimensionar-se dinamicamente torna as operações com coleções rápidas e flexíveis.

As matrizes raramente são usadas diretamente em Go (porque seu comprimento é fixo), mas as fatias estão em toda parte. Para entender as fatias, é preciso primeiro entender as matrizes — em essência, uma fatia é um ponteiro para uma matriz, além de seu comprimento e capacidade. Nesta lição, você vai dominar todos os conceitos fundamentais dos tipos de coleção do Go.

1. Você aprenderá


2. Uma história real de um engenheiro de aquisição de dados

(1) Problema: Um array de tamanho fixo não consegue acomodar dados dinâmicos

Alice é engenheira de aquisição de dados. Recentemente, ela precisou criar um rastreador da web:

“Preciso recuperar 10.000 registros de cada uma das 5 fontes de dados e armazená-los na memória para remover duplicatas. No início, usei listas do Python — bastou um simples ‘append’ e pronto. Depois de mudar para Go, usei um array de comprimento fixo como [10000]int, mas o programa entrou em pânico logo no primeiro registro: ‘índice fora do intervalo’.”

Ela abriu o código do Go que havia escrito:

GO
// Version 1: Fixed-length array; cannot grow
var records [10000]int
for i := 0; i < 50000; i++ {
    records[i] = i  // ❌ i >= 10000 causes panic!
}

O chefe disse: “O número de fontes de dados muda todos os dias, então sua matriz de comprimento fixo não vai funcionar.” Só então Alice percebeu que as matrizes em Go são tipos de valor, e que o comprimento faz parte do tipo — [5]int e [10]int são tipos diferentes.

(2) Solução em Go: usando slices

GO
// data_collector.go
package main

import "fmt"

func main() {
    // Slices: Dynamic length, automatic resizing
    var records []int  // nil slice, can append

    // Simulate retrieving 10,000 records from each of 5 data sources
    for source := 1; source <= 5; source++ {
        for i := 0; i < 10000; i++ {
            value := source*10000 + i
            records = append(records, value)  // auto-resize
        }
    }

    fmt.Printf("Total number of records: %d\n", len(records))
    fmt.Printf("Underlying capacity: %d\n", cap(records))
    fmt.Printf("Top 3: %v\n", records[:3])
    fmt.Printf("Last 3: %v\n", records[len(records)-3:])
}

Resultado:

TEXT 📖 Somente leitura
Total number of records: 50000
Underlying capacity: 65536
Top 3: [10000 10001 10002]
Last 3: [59997 59998 59999]

(3) Desempenho: array x slice

Dimensão matriz [n]T fatia []T
Comprimento Fixo na compilação Variável em tempo de execução
Tipo [5]int[10]int []int Genérico
Passagem de parâmetros Passagem por valor (copia o array inteiro) Passagem por referência (apenas 24 bytes)
Redimensionar Não compatível append automático
Caso de uso Tamanho fixo (por exemplo, buffer) 99% dos casos
💡 Dica: Em Go, as fatias (slices) são usadas em 99% dos cenários de “coleções”; as matrizes (arrays) são usadas apenas quando o tamanho é fixo (como o buffer de 32 bytes para o SHA-256) ou quando é necessária a semântica de valores.


3. matriz

(1) Definição e inicialização de matrizes

GO
package main

import "fmt"

func main() {
    // Method 1: Declaration + Initialization
    var a1 [5]int = [5]int{1, 2, 3, 4, 5}

    // Method 2: Type Inference
    a2 := [5]int{1, 2, 3, 4, 5}

    // Method 3: Partial Initialization (with the Rest Set to Zero)
    a3 := [5]int{1, 2}  // [1 2 0 0 0]

    // Method 4: Use ... to let the compiler infer the length
    a4 := [...]int{1, 2, 3, 4}  // length 4

    // Method 5: Specifying Index Initialization
    a5 := [5]int{0: 10, 4: 50}  // [10 0 0 0 50]

    fmt.Println(a1, a2, a3, a4, a5)
}

Saída:

TEXT 📖 Somente leitura
[1 2 3 4 5] [1 2 3 4 5] [1 2 0 0 0] [1 2 3 4] [10 0 0 0 50]

(2) Iteração por matrizes

GO
package main

import "fmt"

func main() {
    nums := [4]string{"Alice", "Bob", "Charlie", "Dave"}

    // Method 1: for loop
    for i := 0; i < len(nums); i++ {
        fmt.Printf("%d: %s\n", i, nums[i])
    }

    // Method 2: for range
    for i, name := range nums {
        fmt.Printf("[%d] %s\n", i, name)
    }
}

(3) As matrizes são tipos de valor

GO
package main

import "fmt"

func modify(arr [3]int) {
    arr[0] = 999  // modifies the copy
    fmt.Printf("Inside function: %v\n", arr)
}

func main() {
    a := [3]int{1, 2, 3}
    modify(a)
    fmt.Printf("Outside function: %v\n", a)  // original array unchanged
}

Saída:

TEXT 📖 Somente leitura
Inside function: [999 2 3]
Outside function: [1 2 3]
🔥 Erro comum: As matrizes do Go são tipos de valor; portanto, ao passá-las como argumentos, toda a matriz é copiada. Passar uma matriz com 1 milhão de elementos como argumento = copiar 1 milhão de elementos = um desastre de desempenho. Use fatias em vez disso.

(4) ▶ Exemplo: Comparando matrizes (apenas para matrizes do mesmo tipo e comprimento)

GO
package main

import "fmt"

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

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

    // Items of different lengths or types cannot be compared.
    // d := [4]int{1, 2, 3, 4}
    // fmt.Println(a == d)  // ❌ mismatched types
}
▶ Experimente

4. Quatro maneiras de criar fatias

(1) Uma fatia = uma “visão” de um array

100%
graph TB
    subgraph Slice["Slice (24 bytes)"]
        P["ptr<br/>points to the underlying array"]
        L["len<br/>length"]
        C["cap<br/>capacity"]
    end
    subgraph Array["Underlying array"]
        A0["[0]"]
        A1["[1]"]
        A2["[2]"]
        A3["[3]"]
        A4["[4]"]
        A5["[5]"]
    end
    P --> A1
    P -.-> A2
    P -.-> A3

Uma fatia é uma referência à matriz subjacente — ela consiste em apenas 24 bytes (ptr + len + cap). Várias fatias podem compartilhar a mesma matriz subjacente.

(2) Método de criação 1: declarar diretamente uma fatia nula

GO
var s []int  // nil slice, len=0, cap=0
// You can append, but you cannot access it using `s[0]`.

(3) Método de criação 2: Literal

GO
s := []int{1, 2, 3, 4, 5}  // create and initialize
// Base array = [1 2 3 4 5], len = cap = 5

(4) Método de criação 3: make (especifique len e cap)

GO
// Syntax: make([]T, len, cap)
s := make([]int, 5)       // len=5, cap=5, all zero values [0 0 0 0 0]
s := make([]int, 3, 10)   // len=3, cap=10

(5) ▶ Exemplo: Comparação de quatro maneiras de usar “make”

GO
package main

import "fmt"

func main() {
    // Method 1: nil slice
    var s1 []int
    fmt.Printf("s1: len=%d cap=%d nil=%v\n", len(s1), cap(s1), s1 == nil)

    // Method 2: Literal
    s2 := []int{1, 2, 3}
    fmt.Printf("s2: len=%d cap=%d %v\n", len(s2), cap(s2), s2)

    // Method 3: Set the length
    s3 := make([]int, 5)
    fmt.Printf("s3: len=%d cap=%d %v\n", len(s3), cap(s3), s3)

    // Method 4: make (length + capacity)
    s4 := make([]int, 3, 10)
    fmt.Printf("s4: len=%d cap=%d %v\n", len(s4), cap(s4), s4)

    // Method 5: Slicing from an array
    arr := [5]int{10, 20, 30, 40, 50}
    s5 := arr[1:4]  // [20 30 40], len=3 cap=4 (4 remaining elements)
    fmt.Printf("s5: len=%d cap=%d %v\n", len(s5), cap(s5), s5)
}
▶ Experimente

Saída:

TEXT 📖 Somente leitura
s1: len=0 cap=0 nil=true
s2: len=3 cap=3 [1 2 3]
s3: len=5 cap=5 [0 0 0 0 0]
s4: len=3 cap=10 [0 0 0]
s5: len=3 cap=4 [20 30 40]

(6) Referência rápida: 4 maneiras de criar

Método Sintaxe Casos de uso
fatia nula var s []T inicialização preguiçosa
Literal []T{1, 2, 3} Todos os elementos conhecidos
make make([]T, len, cap) Pré-alocar capacidade
Segmentação de matrizes arr[low:high] Visualização de submatrizes

5. len() e cap()

(1) A relação entre len e cap

GO
package main

import "fmt"

func main() {
    s := make([]int, 3, 10)  // len=3, cap=10

    fmt.Printf("Initial: len=%d cap=%d\n", len(s), cap(s))
    // len=3: You can access s[0], s[1], and s[2]
    // cap=10: The underlying array has 10 elements, with room for 7 more to be appended
}

(2) O significado de “cap”

cap - len = o número de elementos que ainda podem ser acrescentados sem alteração do tamanho. Se esse número exceder cap, append alocará uma nova matriz subjacente (normalmente dobrando o tamanho).

(3) ▶ Exemplo: A diferença real entre len e cap

GO
package main

import "fmt"

func main() {
    s := make([]int, 0, 5)  // len=0, cap=5

    for i := 1; i <= 8; i++ {
        s = append(s, i)
        fmt.Printf("append %d: len=%d cap=%d %v\n", i, len(s), cap(s), s)
    }
}
▶ Experimente

Saída:

TEXT 📖 Somente leitura
append 1: len=1 cap=5 [1]
append 2: len=2 cap=5 [1 2]
append 3: len=3 cap=5 [1 2 3]
append 4: len=4 cap=5 [1 2 3 4]
append 5: len=5 cap=5 [1 2 3 4 5]
append 6: len=6 cap=10 [1 2 3 4 5 6]  ← cap doubled
append 7: len=7 cap=10 [1 2 3 4 5 6 7]
append 8: len=8 cap=10 [1 2 3 4 5 6 7 8]
💡 Dica: Estratégia de redimensionamento do Go: Quando len == cap, o novo limite é igual ao antigo * 2 (para fatias pequenas) ou ao antigo * 1,25 (para fatias grandes, aproximadamente > 256).


6. Redimensionamento dinâmico com adição

(1) Noções básicas sobre append

GO
package main

import "fmt"

func main() {
    var s []int  // nil

    s = append(s, 1)       // [1]
    s = append(s, 2, 3, 4) // [1 2 3 4]

    // Batch append: Splitting a slice
    s2 := []int{5, 6, 7}
    s = append(s, s2...)   // [1 2 3 4 5 6 7]

    fmt.Println(s)
}

Saída:

TEXT 📖 Somente leitura
[1 2 3 4 5 6 7]

(2) ▶ Exemplo: Exclusão de elementos usando append (sem a função integrada delete)

GO
package main

import "fmt"

// Delete the element at index i
func removeAt(s []int, i int) []int {
    // Concatenation: s[:i] + s[i+1:]
    return append(s[:i], s[i+1:]...)
}

func main() {
    s := []int{1, 2, 3, 4, 5}
    s = removeAt(s, 2)  // delete 3
    fmt.Println(s)       // [1 2 4 5]
}
▶ Experimente

Saída:

TEXT 📖 Somente leitura
[1 2 4 5]

(3) A armadilha do append: ele pode modificar a matriz original

GO
package main

import "fmt"

func main() {
    s := []int{1, 2, 3, 4, 5}
    s2 := s[:3]  // [1 2 3], shares underlying array

    s2 = append(s2, 99)  // no resize, modified s's underlying array!
    fmt.Println("s:", s)    // [1 2 3 99 5]
    fmt.Println("s2:", s2)  // [1 2 3 99]
}

Saída:

TEXT 📖 Somente leitura
s: [1 2 3 99 5]
s2: [1 2 3 99]
🔥 Erro comum: Essa é uma das armadilhas mais clássicas do Go — as fatias compartilham a matriz subjacente, portanto, chamar append pode afetar outras fatias. Use copy() ou force o redimensionamento quando for necessário um isolamento estrito.


7. Faixa [mín.:máx.:máx.]

(1) Três tipos de expressões de fatia

GO
package main

import "fmt"

func main() {
    arr := [5]int{10, 20, 30, 40, 50}

    // Form 1: [low:high] — Takes [low, high)
    s1 := arr[1:4]
    fmt.Printf("arr[1:4] = %v, len=%d cap=%d\n", s1, len(s1), cap(s1))

    // Format 2: [low:] — From low to the end
    s2 := arr[2:]
    fmt.Printf("arr[2:]  = %v, len=%d cap=%d\n", s2, len(s2), cap(s2))

    // Form 3: [:high] — From the beginning to high
    s3 := arr[:3]
    fmt.Printf("arr[:3]  = %v, len=%d cap=%d\n", s3, len(s3), cap(s3))

    // Format 4: [low:high:max] — Sets a cap (to prevent unexpected additions from affecting the original array)
    s4 := arr[1:3:4]
    fmt.Printf("arr[1:3:4] = %v, len=%d cap=%d\n", s4, len(s4), cap(s4))
}

Saída:

TEXT 📖 Somente leitura
arr[1:4] = [20 30 40], len=3 cap=4
arr[2:]  = [30 40 50], len=3 cap=3
arr[:3]  = [10 20 30], len=3 cap=5
arr[1:3:4] = [20 30], len=2 cap=3

(2) O objetivo de [baixo:alto:máximo]

max define um limite para impedir que append sobrescreva outras partes da matriz original:

GO
package main

import "fmt"

func main() {
    arr := [5]int{10, 20, 30, 40, 50}

    // No limit on max: cap=4; after an append operation, arr[3] will be overwritten
    s1 := arr[1:3]  // [20 30], cap=4
    s1 = append(s1, 999)
    fmt.Println("arr:", arr)  // [10 20 30 999 50] ← overwritten!

    // Limit max=3: cap=2; append creates a new array
    s2 := arr[1:3:3]  // [20 30], cap=2
    s2 = append(s2, 888)
    fmt.Println("arr:", arr)  // [10 20 30 999 50] ← unchanged
    fmt.Println("s2:", s2)    // [20 30 888]
}

Saída:

TEXT 📖 Somente leitura
arr: [10 20 30 999 50]
arr: [10 20 30 999 50]
s2: [20 30 888]

8. Armadilhas do compartilhamento da matriz subjacente ao realizar o slicing (Ponto-chave)

(1) Armadilha 1: A modificação de um elemento de fatia afeta todas as fatias

GO
package main

import "fmt"

func main() {
    arr := [5]int{1, 2, 3, 4, 5}
    s1 := arr[0:3]  // [1 2 3]
    s2 := arr[2:5]  // [3 4 5]

    s1[2] = 999  // same underlying array!
    fmt.Println("s1:", s1)  // [1 2 999]
    fmt.Println("s2:", s2)  // [999 4 5]
    fmt.Println("arr:", arr) // [1 2 999 4 5]
}

(2) Armadilha 2: Usar append com for range causa um loop infinito

GO
package main

import "fmt"

func main() {
    s := []int{1, 2, 3}
    for _, v := range s {
        s = append(s, v*10)  // dangerous: s is growing
    }
    fmt.Println(s)  // output is undefined
}
🔥 Erro comum: Nunca modifique a própria fatia durante um loop for range. Antes do Go 1.22, s em for i, v := range s era um instantâneo do comprimento no início do loop; o Go 1.22 corrigiu isso em alguns cenários, mas ainda é recomendável ter cuidado.

(3) Armadilha 3: As fatias passadas como argumentos de função são modificadas de forma inesperada

GO
package main

import "fmt"

// Slicing uses pass-by-reference, so changes made within the function affect the outside.
func addElement(s []int) {
    s = append(s, 100)  // may modify the original array (if no resize)
    fmt.Printf("Inside function: %v\n", s)
}

func main() {
    s := make([]int, 3, 10)
    s[0], s[1], s[2] = 1, 2, 3

    addElement(s)
    fmt.Printf("Outside function: %v\n", s)  // depends on whether resize occurred
}

(4) Use copy() para resolver problemas de compartilhamento

GO
package main

import "fmt"

func main() {
    arr := [5]int{1, 2, 3, 4, 5}
    s1 := arr[0:3]

    // Create a separate copy
    s2 := make([]int, len(s1))
    copy(s2, s1)

    s1[0] = 999
    fmt.Println("s1:", s1)  // [999 2 3]
    fmt.Println("s2:", s2)  // [1 2 3] ← unaffected
}

(5) ▶ Exemplo: Usando “copiar + acrescentar” para inserir uma fatia

GO
package main

import "fmt"

// Insert element val at index i
func insert(s []int, i int, val int) []int {
    // 1. Add 1 position
    s = append(s, 0)

    // 2. Shift the element at index i and all subsequent elements one position to the right
    copy(s[i+1:], s[i:])

    // 3. Add a new element
    s[i] = val

    return s
}

func main() {
    s := []int{1, 2, 4, 5}
    s = insert(s, 2, 3)
    fmt.Println(s)  // [1 2 3 4 5]
}
▶ Experimente

Saída:

TEXT 📖 Somente leitura
[1 2 3 4 5]

9. Exemplo completo: Coletor de dados dinâmico

Combine todos os recursos das fatias para criar uma ferramenta para coleta de dados de múltiplas fontes + deduplicação + filtragem:

GO
// collector.go
package main

import (
    "fmt"
    "strings"
)

// Data Source Simulation Functions
func fetchFromSource(sourceName string) []string {
    data := map[string][]string{
        "GitHub": {"repo:go", "repo:docker", "issue:bug", "repo:go"},  // last one is duplicate
        "NPM":    {"pkg:react", "pkg:vue", "pkg:react"},              // duplicate
        "PyPI":   {"pkg:requests", "pkg:flask"},
    }
    return data[sourceName]
}

// Removing Duplicates: Using `map` to Create a Hash Set
func deduplicate(data []string) []string {
    seen := make(map[string]bool)
    result := make([]string, 0, len(data))

    for _, item := range data {
        if !seen[item] {
            seen[item] = true
            result = append(result, item)
        }
    }
    return result
}

// Filter: Keep only those starting with "repo:"
func filterRepos(data []string) []string {
    result := make([]string, 0)
    for _, item := range data {
        if strings.HasPrefix(item, "repo:") {
            result = append(result, item)
        }
    }
    return result
}

// Pipeline: Combining Multiple Steps
func collectPipeline() (total, unique, repos int, sources []string) {
    defer func() {
        // Naming Return Values + Defer Collection Statistics
        fmt.Printf("\n[Collection Complete] total=%d unique=%d repos=%d sources=%v\n",
            total, unique, repos, sources)
    }()

    allSources := []string{"GitHub", "NPM", "PyPI"}
    var allData []string

    for _, src := range allSources {
        sources = append(sources, src)
        data := fetchFromSource(src)
        allData = append(allData, data...)
        total += len(data)
    }

    uniqueData := deduplicate(allData)
    unique = len(uniqueData)

    repoData := filterRepos(uniqueData)
    repos = len(repoData)

    fmt.Println("\n=== Deduped Data ===")
    for _, item := range uniqueData {
        fmt.Printf("  %s\n", item)
    }

    fmt.Println("\n=== Filtering Repo Data ===")
    for _, item := range repoData {
        fmt.Printf("  %s\n", item)
    }

    return total, unique, repos, sources
}

func main() {
    collectPipeline()
}

Resultado esperado:

TEXT 📖 Somente leitura
=== Deduped Data ===
  repo:go
  repo:docker
  issue:bug
  pkg:react
  pkg:vue
  pkg:requests
  pkg:flask

=== Filtering Repo Data ===
  repo:go
  repo:docker

[Collection Complete] total=9 unique=7 repos=2 sources=[GitHub NPM PyPI]
🔥 Erro comum: Use make([]string, 0, len(data)) para pré-alocar capacidade e evitar redimensionamentos frequentes com append. Essa é uma técnica fundamental de otimização de desempenho.


❓ Perguntas Frequentes

P: Qual é a relação subjacente entre matrizes e fatias? R: Uma fatia é, essencialmente, uma visão de uma matriz. Uma fatia consiste em 24 bytes: um ponteiro de 8 bytes (que aponta para um elemento da matriz) + um comprimento de 8 bytes + uma capacidade de 8 bytes. Várias fatias podem compartilhar a mesma matriz subjacente.

P: O comando append sempre faz com que o array seja redimensionado? R: Não necessariamente. O array só é redimensionado (alocando-se um novo array e copiando-se o conteúdo duas vezes) quando se executa len == cap. Se ainda houver espaço disponível, os elementos são gravados diretamente no array original.

P: Uma fatia é passada para uma função por valor ou por referência? R: A fatia em si (24 bytes) é passada por valor, mas a matriz subjacente para a qual ela aponta é compartilhada. Portanto, modificar elementos dentro da função afeta a fatia externa, mas o fato de append afetar a fatia externa depende de a fatia ter sido redimensionada ou não.

P: Quando é necessário usar o parâmetro max no formato [low:high:max]? R: Use-o quando quiser limitar o tamanho de uma fatia para evitar que operações append afetem outros elementos da matriz original. Cenário comum: criar uma subfatia a partir de uma fatia maior, seguida por uma operação append.

P: Qual é a diferença entre uma fatia nil e uma fatia vazia? R: Uma fatia nil (como var s []int) resulta em true quando verificada em relação a nil; seu len é 0 e seu cap é 0. Uma fatia vazia (como []int{} ou make([]int, 0)) resulta em falso quando comparada a nil, com len=0 e cap=0. É possível acrescentar elementos a ambas, mas os resultados da serialização JSON diferem: nilnull, vazio → [].

P: Existe um método embutido para remover elementos de uma fatia? R: Não. Ao contrário do Java, o Go não possui list.remove(i). O Go implementa a remoção usando append(s[:i], s[i+1:]...) (mas isso não preserva a ordem). Se você quiser preservar a ordem, precisará reordenar os elementos manualmente.

P: Como faço para copiar uma fatia? R: Existem três maneiras: (1) copy(dst, src) — Copia min(len(dst), len(src)) elementos; (2) append([]T(nil), src...) — Copia todos os elementos; (3) Manualmente, usando for range.

P: Qual é a diferença de desempenho entre fatias e matrizes ao usar range? R: Usar range em uma matriz copia a matriz inteira (baixo desempenho); usar range em uma fatia copia apenas 24 bytes (bom desempenho). É por isso que as fatias são o padrão de fato para “coleções”.


📖 Resumo


📝 Exercícios

  1. Problema básico (Dificuldade ⭐): Use fatias para implementar uma função reverse([]int) []int que inverta os elementos de uma fatia. Dica: Use um loop for com uma variável temporária ou troque os elementos no próprio local. Caso de teste: reverse([]int{1,2,3,4,5})[5 4 3 2 1].

  2. Problema Avançado (Dificuldade ⭐⭐): Implemente uma função uniqueSorted(nums []int) []int: remova duplicatas e classifique em ordem crescente. Dica: Primeiro, classifique a matriz usando sort.Ints; em seguida, percorra-a para remover as duplicatas. Dada a entrada []int{3, 1, 4, 1, 5, 9, 2, 6, 5}, a saída deve ser [1 2 3 4 5 6 9].

  3. Problema de desafio (Dificuldade ⭐⭐⭐): Implemente um algoritmo de máximo por janela deslizante: maxSlidingWindow(nums []int, k int) []int. Por exemplo, nums=[1,3,-1,-3,5,3,6,7], k=3 → Saída [3,3,5,5,6,7]. Requisitos: (1) Complexidade temporal O(n); (2) Use um deque (que pode ser simulado usando []int) para manter a janela; (3) Explique por que uma percorrida direta resultaria em um tempo limite esgotado (O(n*k)).

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%