Carregando
Todos os artigos
Programação 15 min de leitura

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.

nbkhope
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] 4

Um 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
  • grade[0] é a primeira linha, um []string inteiro.
  • grade[0][1] pega a linha 0 e depois a coluna 1 dentro dela: o X. 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 20

Uma 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

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
Array [3][3]stringSlice [][]string
TamanhoFixo, faz parte do tipoPode crescer com append
LinhasSempre do mesmo tamanhoPodem ser diferentes (irregular)
Atribuir copia…todos os valoressó os cabeçalhos: as duas dividem os dados
Valor zeroUma grade cheia de valores zeronil (sem linhas)
Uso típicoTabuleiros fixos pequenos, coordenadasA 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:

A ordem importa: somando uma grade de 4.000 por 4.000 ints no Go 1.22 no Windows, mediana de 3 execuções do testing.Benchmark. Linha por linha num slice de slices: 13,1 milissegundos. A mesma grade como um único slice plano, com índice linha vezes largura mais coluna: 19,3 milissegundos, mais ou menos o mesmo. Coluna por coluna no slice de slices: 170,1 milissegundos, 13 vezes mais lento. O processador carrega a memória em linhas de cache de 64 bytes; ao longo de uma linha, todo número de cada pedaço carregado é usado, enquanto descendo uma coluna cada número vem da memória de uma linha diferente
OrdemTempo
Linha por linha13,1 ms
Um slice plano, linha por linha19,3 ms
Coluna por coluna170,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

  1. Em grade[2][0], qual índice escolhe a linha?
  2. O que make([][]int, 3) cria, e o que fica faltando?
  3. Por que mudar compartilhada[0][0] mudou todas as linhas na Armadilha 2?
  4. Qual é a diferença entre atribuir um [3][3]string e um [][]string?
  5. 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: 22

Exercí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 1

Exercí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ém

Cada 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 literalgrade := [][]string{{"O", "X"}, {"-", "O"}}
Ler / mudar uma casagrade[linha][coluna], grade[linha][coluna] = "X"
Contar linhas / casas de uma linhalen(grade) / len(grade[linha])
Uma grade vazia de linhas × colunasmake([][]int, linhas) e depois grade[linha] = make([]int, colunas) para cada linha
Percorrer todas as casasfor linha := range grade { for coluna := range grade[linha] { … } }
Acrescentar uma linhagrade = append(grade, novaLinha)
Copiar uma gradecrie cada linha e copy(clone[linha], grade[linha])
Um tabuleiro de tamanho fixovar tabuleiro [3][3]string
O percurso mais rápidolaç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 make leva 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]string sã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.

Obrigado pela leitura!

Conhece alguém que vai gostar? Mande para essa pessoa.

Mais artigos

Leia a seguir

Programação 5 min de leitura

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.

nbkhope

Comentários

Seja gentil e específico. Comentários com links de contas novas aparecem depois da análise de um moderador.