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á
- Definir, inicializar, percorrer e comparar matrizes
- 4 maneiras de criar fatias (literal/matriz/make/valor nulo)
- A diferença entre
len()ecap() - Os significados dos três argumentos de
make appendmecanismo de redimensionamento automático- Fatiar
[low:high:max] - Armadilhas do compartilhamento da matriz subjacente na divisão em fatias (Ponto-chave)
- Implementação de um coletor de dados dinâmico utilizando fatias
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:
// 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
// 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:
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 |
3. matriz
(1) Definição e inicialização de matrizes
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:
[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
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
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:
Inside function: [999 2 3]
Outside function: [1 2 3]
(4) ▶ Exemplo: Comparando matrizes (apenas para matrizes do mesmo tipo e comprimento)
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
}
4. Quatro maneiras de criar fatias
(1) Uma fatia = uma “visão” de um array
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
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
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)
// 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”
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)
}
Saída:
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
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
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)
}
}
Saída:
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]
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
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:
[1 2 3 4 5 6 7]
(2) ▶ Exemplo: Exclusão de elementos usando append (sem a função integrada delete)
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]
}
Saída:
[1 2 4 5]
(3) A armadilha do append: ele pode modificar a matriz original
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:
s: [1 2 3 99 5]
s2: [1 2 3 99]
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
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:
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:
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:
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
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
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
}
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
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
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
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]
}
Saída:
[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:
// 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:
=== 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]
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
appendsempre 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 executalen == 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
appendafetar a fatia externa depende de a fatia ter sido redimensionada ou não.
P: Quando é necessário usar o parâmetro
maxno formato[low:high:max]? R: Use-o quando quiser limitar o tamanho de uma fatia para evitar que operaçõesappendafetem outros elementos da matriz original. Cenário comum: criar uma subfatia a partir de uma fatia maior, seguida por uma operaçãoappend.
P: Qual é a diferença entre uma fatia
nile uma fatia vazia? R: Uma fatianil(comovar s []int) resulta emtruequando verificada em relação anil; seulené 0 e seucapé 0. Uma fatia vazia (como[]int{}oumake([]int, 0)) resulta em falso quando comparada anil, comlen=0ecap=0. É possível acrescentar elementos a ambas, mas os resultados da serialização JSON diferem:nil→null, 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 usandoappend(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, usandofor range.
P: Qual é a diferença de desempenho entre fatias e matrizes ao usar
range? R: Usarrangeem uma matriz copia a matriz inteira (baixo desempenho); usarrangeem uma fatia copia apenas 24 bytes (bom desempenho). É por isso que as fatias são o padrão de fato para “coleções”.
📖 Resumo
- As matrizes são tipos de valor (comprimento fixo, passadas por cópia), enquanto as fatias são tipos de referência (dinâmicas, passadas por compartilhamento dos dados subjacentes).
- 4 maneiras de criar fatias: nil / literais / make / fatias de array
lené o número de elementos visíveis,capé a capacidade da matriz subjacente eappendredimensiona a matriz quandolen == cap(estratégia de duplicação)- A expressão de fatia de três parâmetros
[low:high:max]define um limite paracapa fim de impedir acréscimos fora dos limites - Compartilhar o array subjacente em fatias leva a três armadilhas clássicas: modificações em elementos que afetam uns aos outros / adição dentro de um
range/ efeitos colaterais da adição por meio de funções - Soluções:
copy()para criar uma cópia separada / forçar o redimensionamento (append(s, 0)[:0]) / limitar o tamanho máximo - Em projetos reais, 99% das vezes são utilizados intervalos de tempo; as matrizes são utilizadas apenas em cenários que envolvem tamanhos fixos ou semântica de valores.
📝 Exercícios
-
Problema básico (Dificuldade ⭐): Use fatias para implementar uma função
reverse([]int) []intque inverta os elementos de uma fatia. Dica: Use um loopforcom 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]. -
Problema Avançado (Dificuldade ⭐⭐): Implemente uma função
uniqueSorted(nums []int) []int: remova duplicatas e classifique em ordem crescente. Dica: Primeiro, classifique a matriz usandosort.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]. -
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 umdeque(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)).