Slices de Duas Dimensões (Matrizes) em Go: Um Guia Visual e Prático
Monte grades e matrizes em Go com slices de slices: literais, make, laços aninhados, linhas de tamanhos diferentes, arrays vs. slices, as armadilhas da linha compartilhada e do make esquecido, um benchmark real mostrando por que a ordem das linhas é 13x mais rápida, e exercícios.

Nesta página
Um tabuleiro de jogo da velha, uma planilha, o mapa de um jogo, os pixels de uma imagem: muitos dados vêm em linhas e colunas. Em Go você guarda uma grade assim num slice de duas dimensões, um slice cujos elementos também são slices.
Neste guia você vai montar grades com literais e com make, percorrê-las com laços aninhados, fazer linhas de tamanhos diferentes e aprender a diferença entre slices de slices e os arrays de tamanho fixo do Go. Você também vai cair nas duas armadilhas clássicas (linhas que no fundo são a mesma linha, e linhas que nunca foram criadas) e ver um benchmark real em que só trocar dois laços de lugar deixou o mesmo trabalho 13 vezes mais lento. Exercícios, incluindo achar o vencedor do jogo da velha, fecham o artigo.
Todas as saídas abaixo vêm de execuções reais com o Go 1.22 no Windows.
Relembrando: slices de uma dimensão
Um slice (uma "fatia") é uma lista que pode crescer, com valores de um único tipo, escrita []tipo:
1package main
2
3import "fmt"
4
5func main() {
6 nomes := []string{"João", "Maria", "Zeus"}
7 fmt.Println(nomes[0], len(nomes))
8 nomes = append(nomes, "Ana")
9 fmt.Println(nomes, len(nomes))
10}João 3
[João Maria Zeus Ana] 4Um slice de slices
Acrescente mais um par de colchetes e cada elemento também vira um slice: [][]string é "um slice de slices de strings". Eis um tabuleiro de jogo da velha:
1package main
2
3import (
4 "fmt"
5 "strings"
6)
7
8func main() {
9 grade := [][]string{
10 {"O", "X", "-"},
11 {"-", "O", "-"},
12 {"-", "-", "X"},
13 }
14
15 fmt.Println(grade[0])
16 fmt.Println(grade[0][1])
17 fmt.Println(len(grade), "linhas,", len(grade[0]), "colunas")
18
19 grade[2][0] = "O"
20 for _, linha := range grade {
21 fmt.Println(strings.Join(linha, " "))
22 }
23}1[O X -]
2X
33 linhas, 3 colunas
4O X -
5- O -
6O - X![Um slice de duas dimensões é um slice de linhas. grade é um [][]string com 3 elementos, grade[0], grade[1] e grade[2], e cada elemento é ele mesmo uma linha []string de 3 casas. grade[0] é a primeira linha inteira, O X -. grade[0][1] escolhe a linha 0 e depois a coluna 1 dentro dela: X. O primeiro índice escolhe a linha, o segundo escolhe a coluna dentro dessa linha. len(grade) conta as linhas; len(grade[0]) conta as casas da primeira linha](https://nbkmundotech.us-southeast-1.linodeobjects.com/static/images/88b64dee-12a8-47d2-bbb7-c56ef229d349.png?v=1792886256817)
grade[0]é a primeira linha, um[]stringinteiro.grade[0][1]pega a linha 0 e depois a coluna 1 dentro dela: oX. O primeiro índice escolhe a linha, o segundo escolhe a coluna.len(grade)conta as linhas;len(grade[0])conta as casas de uma linha.
Dentro do literal, as linhas de dentro não precisam repetir o tipo: {"O", "X", "-"} é o atalho de []string{"O", "X", "-"}. (O gofmt -s reescreve a forma longa na curta para você.)
Montando uma grade com make
Quando você não sabe os valores de antemão, crie a grade com make. São dois passos: crie o slice de fora, com as linhas, e depois crie cada linha:
1package main
2
3import "fmt"
4
5func main() {
6 linhas, colunas := 4, 5
7 tabela := make([][]int, linhas)
8 for linha := range tabela {
9 tabela[linha] = make([]int, colunas)
10 }
11
12 for linha := range tabela {
13 for coluna := range tabela[linha] {
14 tabela[linha][coluna] = (linha + 1) * (coluna + 1)
15 }
16 }
17
18 for _, fileira := range tabela {
19 for _, valor := range fileira {
20 fmt.Printf("%3d", valor)
21 }
22 fmt.Println()
23 }
24}1 1 2 3 4 5
2 2 4 6 8 10
3 3 6 9 12 15
4 4 8 12 16 20Uma tabuada: o laço de fora percorre as linhas, o de dentro percorre as colunas de cada linha. Linhas novas começam cheias de valores zero (0 para int), e o %3d ocupa 3 caracteres com cada número para as colunas ficarem alinhadas.
Armadilha 1: esquecer de criar as linhas
make([][]int, 2) cria 2 linhas, mas cada linha é um slice nil, sem casa nenhuma:
1package main
2
3import "fmt"
4
5func main() {
6 grade := make([][]int, 2)
7 fmt.Println(grade, len(grade), grade[0] == nil)
8 grade[0][0] = 1
9}$ go run .
[[] []] 2 true
panic: runtime error: index out of range [0] with length 0(seguido de um rastro da pilha). A linha 0 tem tamanho 0, então não existe a casa 0 para escrever ("índice fora do intervalo"). Sempre rode o segundo passo, grade[linha] = make([]int, colunas), para toda linha.
Armadilha 2: linhas que no fundo são a mesma linha
1package main
2
3import "fmt"
4
5func main() {
6 linha := make([]int, 3)
7 compartilhada := [][]int{linha, linha, linha}
8 compartilhada[0][0] = 7
9 fmt.Println(compartilhada)
10}[[7 0 0] [7 0 0] [7 0 0]]![Por que as linhas compartilhadas mudaram todas. Um valor de slice é um cabeçalho pequeno, de 24 bytes em sistemas de 64 bits: um ponteiro para um array por baixo, um tamanho e uma capacidade. Em compartilhada := [][]int{linha, linha, linha}, os três cabeçalhos apontam para o mesmo array, então escrever 7 em compartilhada[0][0] muda o que as três linhas veem. Com um make por linha, cada cabeçalho aponta para o seu próprio array, e as linhas são independentes](https://nbkmundotech.us-southeast-1.linodeobjects.com/static/images/93f52346-38cb-480b-9631-7bd41cb65685.png?v=1792886256893)
As três linhas mudaram, porque um slice não guarda os elementos diretamente: ele é um cabeçalho pequeno (um ponteiro para um array por baixo, mais um tamanho e uma capacidade), e os três cabeçalhos apontam para o mesmo array. Dê a cada linha o seu próprio make (ou faça append de um slice novo) e elas ficam independentes.
Linhas de tamanhos diferentes
Nada obriga as linhas a terem o mesmo tamanho. Um slice irregular (jagged) é perfeito para formas como o triângulo de Pascal, em que cada número é a soma dos dois acima dele:
1package main
2
3import "fmt"
4
5func main() {
6 triangulo := [][]int{}
7 for nivel := 0; nivel < 6; nivel++ {
8 linha := make([]int, nivel+1)
9 linha[0], linha[nivel] = 1, 1
10 for posicao := 1; posicao < nivel; posicao++ {
11 linha[posicao] = triangulo[nivel-1][posicao-1] + triangulo[nivel-1][posicao]
12 }
13 triangulo = append(triangulo, linha)
14 }
15 for _, linha := range triangulo {
16 fmt.Println(linha)
17 }
18}1[1]
2[1 1]
3[1 2 1]
4[1 3 3 1]
5[1 4 6 4 1]
6[1 5 10 10 5 1]Cada linha nova é montada a partir da anterior e depois acrescentada ao triângulo. Como as linhas podem ser diferentes, percorra com len(triangulo[linha]) (ou range), nunca com um número fixo de colunas.
Arrays vs. slices
O Go também tem arrays de tamanho fixo, em que o tamanho faz parte do tipo: [3][3]string é sempre exatamente 3 por 3. A grande diferença aparece na cópia:
1package main
2
3import "fmt"
4
5func main() {
6 var tabuleiro [3][3]string
7 copiaDoTabuleiro := tabuleiro
8 copiaDoTabuleiro[1][1] = "X"
9 fmt.Printf("%q %q\n", tabuleiro[1], copiaDoTabuleiro[1])
10
11 grade := [][]string{{"", "", ""}, {"", "", ""}}
12 mesmaGrade := grade
13 mesmaGrade[1][1] = "X"
14 fmt.Printf("%q %q\n", grade[1], mesmaGrade[1])
15}["" "" ""] ["" "X" ""]
["" "X" ""] ["" "X" ""]![Atribuir um array vs. um slice. copiaDoTabuleiro := tabuleiro, com um [3][3]string: cada casa é copiada, então são dois tabuleiros independentes, e marcar um X na cópia não muda o original. mesmaGrade := grade, com um [][]string: só os cabeçalhos das linhas são copiados, então as duas variáveis dividem as mesmas casas, e o X aparece nas duas. Para copiar de verdade uma grade de slices, faça o make de cada linha e use copy](https://nbkmundotech.us-southeast-1.linodeobjects.com/static/images/39eb7976-9471-4d5b-9d44-38c274e94ba9.png?v=1792886256918)
Array [3][3]string | Slice [][]string | |
|---|---|---|
| Tamanho | Fixo, faz parte do tipo | Pode crescer com append |
| Linhas | Sempre do mesmo tamanho | Podem ser diferentes (irregular) |
| Atribuir copia… | todos os valores | só os cabeçalhos: as duas dividem os dados |
| Valor zero | Uma grade cheia de valores zero | nil (sem linhas) |
| Uso típico | Tabuleiros fixos pequenos, coordenadas | A maioria das grades e matrizes |
Atribuir um array copia cada casa, então o copiaDoTabuleiro é independente. Atribuir um slice copia só os cabeçalhos, então mesmaGrade e grade dividem as mesmas linhas. Para copiar de verdade uma grade de slices, copie cada linha:
1package main
2
3import "fmt"
4
5func main() {
6 grade := [][]int{{1, 2}, {3, 4}}
7 clone := make([][]int, len(grade))
8 for linha := range grade {
9 clone[linha] = make([]int, len(grade[linha]))
10 copy(clone[linha], grade[linha])
11 }
12 clone[0][0] = 99
13 fmt.Println(grade, clone)
14}[[1 2] [3 4]] [[99 2] [3 4]]Linhas do range dividem as casas
No guia do laço for, mudar a cópia de um int vinda do range não mudava o slice. Com linhas é diferente: a cópia é uma cópia do cabeçalho, que continua apontando para as mesmas casas:
1package main
2
3import "fmt"
4
5func main() {
6 grade := [][]int{{1, 2}, {3, 4}}
7 for _, linha := range grade {
8 linha[0] = 0
9 }
10 fmt.Println(grade)
11}[[0 2] [0 4]]Linha por linha é 13 vezes mais rápido
O jeito de percorrer uma grade importa mais do que parece. Somamos uma grade de 4.000 × 4.000 ints (16 milhões de casas) de dois jeitos: linha por linha (for linha por fora, for coluna por dentro) e coluna por coluna (laços trocados). As mesmas casas, as mesmas somas:

| Ordem | Tempo |
|---|---|
| Linha por linha | 13,1 ms |
| Um slice plano, linha por linha | 19,3 ms |
| Coluna por coluna | 170,1 ms |
Coluna por coluna foi 13 vezes mais lento. O processador não busca os números um por um: ele carrega a memória em pedaços chamados linhas de cache (64 bytes, 8 ints). Andando ao longo de uma linha, todo número do pedaço carregado é usado; descendo uma coluna, cada número precisa da memória de uma linha diferente, e o resto do pedaço é jogado fora. Então: o laço das linhas por fora e o das colunas por dentro. (Um único slice plano, com índice linha*largura + coluna, ficou na mesma faixa do slice de slices aqui; ele economiza um salto de ponteiro por linha, mas paga uma multiplicação.)
Sua vez: pratique
Tente fazer estes antes de espiar as respostas. Rode cada um com go run ..
Exercício 1: Adivinhe a saída (aquecimento). O que isto imprime?
1package main
2
3import "fmt"
4
5func main() {
6 grade := [][]int{
7 {1, 2, 3},
8 {4, 5},
9 {6},
10 }
11 fmt.Println(len(grade), len(grade[1]), grade[1][1], grade[2][0])
12 grade[0] = append(grade[0], 10)
13 fmt.Println(grade)
14}Exercício 2: Total de cada linha. Dado notas := [][]int{{8, 9, 7}, {6, 10, 9}, {7, 7, 8}} (três alunos, três provas), imprima o total de cada aluno.
Exercício 3: Ache o máximo. Na mesma grade notas, ache a maior nota e imprima a linha e a coluna dela.
Exercício 4: Transposta. Escreva um programa que transforme [][]int{{1, 2, 3}, {4, 5, 6}} (2 linhas, 3 colunas) na sua transposta (3 linhas, 2 colunas), em que as linhas viram colunas.
Exercício 5: Vencedor do jogo da velha (desafio). Escreva uma função vencedor(grade [][]string) string que devolva "X", "O" ou "ninguém", conferindo todas as linhas, colunas e as duas diagonais.
Quiz rápido
- Em
grade[2][0], qual índice escolhe a linha? - O que
make([][]int, 3)cria, e o que fica faltando? - Por que mudar
compartilhada[0][0]mudou todas as linhas na Armadilha 2? - Qual é a diferença entre atribuir um
[3][3]stringe um[][]string? - Por que percorrer uma grade coluna por coluna é mais lento?
Respostas
Exercício 1: As linhas têm 3, 2 e 1 casas; o append aumenta só a primeira linha:
1package main
2
3import "fmt"
4
5func main() {
6 grade := [][]int{
7 {1, 2, 3},
8 {4, 5},
9 {6},
10 }
11 fmt.Println(len(grade), len(grade[1]), grade[1][1], grade[2][0])
12 grade[0] = append(grade[0], 10)
13 fmt.Println(grade)
14}3 2 5 6
[[1 2 3 10] [4 5] [6]]Exercício 2: Some cada linha no laço de dentro:
1package main
2
3import "fmt"
4
5func main() {
6 notas := [][]int{{8, 9, 7}, {6, 10, 9}, {7, 7, 8}}
7 for aluno, provas := range notas {
8 total := 0
9 for _, nota := range provas {
10 total += nota
11 }
12 fmt.Printf("Aluno %d: %d\n", aluno+1, total)
13 }
14}Aluno 1: 24
Aluno 2: 25
Aluno 3: 22Exercício 3: Guarde o melhor valor e onde ele foi achado:
1package main
2
3import "fmt"
4
5func main() {
6 notas := [][]int{{8, 9, 7}, {6, 10, 9}, {7, 7, 8}}
7 maior, linhaDoMaior, colunaDoMaior := notas[0][0], 0, 0
8 for linha := range notas {
9 for coluna := range notas[linha] {
10 if notas[linha][coluna] > maior {
11 maior, linhaDoMaior, colunaDoMaior = notas[linha][coluna], linha, coluna
12 }
13 }
14 }
15 fmt.Printf("Maior: %d na linha %d, coluna %d\n", maior, linhaDoMaior, colunaDoMaior)
16}Maior: 10 na linha 1, coluna 1Exercício 4: O resultado tem tantas linhas quanto o original tem colunas; a casa [linha][coluna] vai para [coluna][linha]:
1package main
2
3import "fmt"
4
5func main() {
6 matriz := [][]int{{1, 2, 3}, {4, 5, 6}}
7 transposta := make([][]int, len(matriz[0]))
8 for coluna := range transposta {
9 transposta[coluna] = make([]int, len(matriz))
10 for linha := range matriz {
11 transposta[coluna][linha] = matriz[linha][coluna]
12 }
13 }
14 fmt.Println(transposta)
15}[[1 4] [2 5] [3 6]]Exercício 5: Confira cada trio em linha, ignorando as casas vazias:
1package main
2
3import "fmt"
4
5func vencedor(grade [][]string) string {
6 trios := [][][2]int{
7 {{0, 0}, {0, 1}, {0, 2}}, {{1, 0}, {1, 1}, {1, 2}}, {{2, 0}, {2, 1}, {2, 2}},
8 {{0, 0}, {1, 0}, {2, 0}}, {{0, 1}, {1, 1}, {2, 1}}, {{0, 2}, {1, 2}, {2, 2}},
9 {{0, 0}, {1, 1}, {2, 2}}, {{0, 2}, {1, 1}, {2, 0}},
10 }
11 for _, casas := range trios {
12 primeira := grade[casas[0][0]][casas[0][1]]
13 if primeira == "-" {
14 continue
15 }
16 if grade[casas[1][0]][casas[1][1]] == primeira && grade[casas[2][0]][casas[2][1]] == primeira {
17 return primeira
18 }
19 }
20 return "ninguém"
21}
22
23func main() {
24 fmt.Println(vencedor([][]string{{"O", "X", "-"}, {"-", "O", "-"}, {"-", "X", "O"}}))
25 fmt.Println(vencedor([][]string{{"X", "X", "X"}, {"O", "O", "-"}, {"-", "-", "-"}}))
26 fmt.Println(vencedor([][]string{{"X", "O", "X"}, {"X", "O", "O"}, {"O", "X", "X"}}))
27}O
X
ninguémCada trio é uma lista de três coordenadas [2]int (linha, coluna), então as oito linhas vencedoras cabem numa tabela só, em vez de oito if separados.
Quiz: (1) O primeiro, o 2; o segundo escolhe a coluna. (2) Um slice de 3 linhas, cada uma um slice nil sem casas; ainda falta fazer o make de cada linha. (3) As três linhas eram o mesmo cabeçalho de slice, apontando para um único array. (4) Atribuir um array copia todos os valores; atribuir um slice de slices copia só os cabeçalhos, então as duas variáveis dividem as linhas. (5) O processador carrega a memória em linhas de cache de 64 bytes; ao longo de uma linha, todo número carregado é usado, mas descendo uma coluna cada número vem da memória de uma linha diferente. No nosso teste foi 13 vezes mais lento.
Cola rápida
| Eu quero… | Escreva |
|---|---|
| Uma grade literal | grade := [][]string{{"O", "X"}, {"-", "O"}} |
| Ler / mudar uma casa | grade[linha][coluna], grade[linha][coluna] = "X" |
| Contar linhas / casas de uma linha | len(grade) / len(grade[linha]) |
| Uma grade vazia de linhas × colunas | make([][]int, linhas) e depois grade[linha] = make([]int, colunas) para cada linha |
| Percorrer todas as casas | for linha := range grade { for coluna := range grade[linha] { … } } |
| Acrescentar uma linha | grade = append(grade, novaLinha) |
| Copiar uma grade | crie cada linha e copy(clone[linha], grade[linha]) |
| Um tabuleiro de tamanho fixo | var tabuleiro [3][3]string |
| O percurso mais rápido | laço das linhas por fora, das colunas por dentro |
Resumindo
Slices de duas dimensões são só slices de linhas:
[][]Té um slice de slices:grade[linha][coluna], o primeiro índice é a linha, o segundo a coluna.- Montar um com
makeleva dois passos: o slice de fora e depois cada linha. Pular o segundo passo dá pânico com "index out of range". - As linhas são cabeçalhos que apontam para os dados, então reaproveitar uma linha faz todas mudarem juntas, e atribuir uma grade a compartilha; copie linha por linha para uma cópia de verdade.
- As linhas podem ter tamanhos diferentes, como no triângulo de Pascal.
- Arrays como
[3][3]stringsão valores de tamanho fixo que se copiam inteiros quando atribuídos. - Percorra linha por linha: no nosso benchmark, coluna por coluna foi 13 vezes mais lento com exatamente os mesmos dados.
Voltando pela série: O Laço For em Go, Como Definir uma Variável em Go e Seu Primeiro Programa em Go. A seguir: adiando trabalho com o defer em Go.
Leia a seguir

std::vector em C++: Tamanho, Capacidade e Crescimento Seguro
Entenda como std::vector cresce em C++: a diferença entre tamanho e capacidade, quando usar reserve, por que resize é diferente e como evitar referências inválidas.

Como Tratar Erros em Go: Um Guia Prático
Aprenda a tratar erros em Go com exemplos executáveis: retorne um erro, confira o resultado, acrescente contexto com %w e reconheça um arquivo ausente com errors.Is.

O Que É a Linguagem de Programação Rust? Um Guia Simples da Linguagem Rápida Que Não Deixa Você Travar
Um guia simples e sem jargões sobre o Rust, a linguagem tão rápida quanto o C que impede famílias inteiras de erros de memória antes de o programa rodar. Entenda a ideia de posse, por que gigantes da tecnologia o adotam e onde ele é usado.
Comentários
Seja gentil e específico. Comentários com links de contas novas aparecem depois da análise de um moderador.