#6 GoSchool: Estrutura de Dados e seus Métodos
Seja bem vindo ao tutorial #6 da GoSchool!
Caso você tenha perdido o tutorial #5 sobre Estruturas de Controle, segue o link:
Vamos falar sobre estrutura de dados abordando a sua vasta utilização em qualquer cenário de desenvolvimento.
Os tópicos abordados durante este tutorial são:
- Array
- Slice
- Map
- “Arra… o quê? ”
Calma lá, vamos por partes!
Primeiro, precisamos entender que as estrutura de dados faz referência a um grupo de informações, que por sua vez, podem ser manipuladas, consultadas, alteradas e eliminadas.
Não ficou claro?
Vamos pensar na seguinte hipótese:
8 Gophers estão de mãos dadas. Cada um deles possui uma posição especifica na fila (representado através dos números), e cada um possui um valor próprio, podendo ou não se repetir (representado através das cores).
Então ficamos assim:
- Tipo = Gopher;
- Valor = Cor;
- Posição = Número.
Por algum motivo, quero eliminar apenas os Gophers que estão em posições de números primos.
Note que o valor foi removido, tornando nossos Gophers “nulos”. Contudo, sua posição e tipo foram mantidas, dessa forma, capazes de serem “reutilizadas”.
AGORA SIM!
- Posições que quando criadas ocupam um espaço deixando “rastros”;
- Valores de mesmo tipo que podem ou não se repetir;
- Ordenação bem definida.
Esse é o princípio para que possamos entender o funcionamento das estruturas de dados.
Cada tipo possui suas peculiaridades, que serão abordadas a partir de agora.
1. Array
Aqui estamos, no “pai” das nossas estruturas de dados. O responsável por organizar e armazenar valores do mesmo tipo.
O array
pode ser relacionado a uma lista de valores de mesmo tipo e tamanho pré-definido.
- “Como utilizo esse tal de array?”
Antes de tudo, se você nunca teve contato com programação, precisamos reestruturar o modo como você conta.
SIM, O MODO COMO VOCÊ CONTA!
Naturalmente, aprendemos a contar da seguinte forma:
1, 2, 3, 4, 5, 6 …
Por uma convenção definida a muitos anos (A long long time ago…), uma das práticas na programação é iniciar a contagem de índices a partir do 0.
0, 1, 2, 3, 4, 5, 6 …
Brincadeiras a parte, existe sim uma explicação para o motivo dessa convenção, contudo não vem ao caso neste tutorial devido ao nosso escopo. Caso queira aprender sobre, da uma olhadinha AQUI na resposta de uma pergunta do GUJ, para uma breve explicação.
Agora que entendemos como “contar”, podemos entrar de vez na utilização do array
.
Dicionário
Vamos começar com o dicionário do array
:
- Array - Estrutura de armazenamento de dados em índices;
- Índice - Posição de um valor no
array
; - Capacidade - Volume de índices suportados pelo
array
.
Sua declaração é feita com a seguinte estrutura:
var array [capacidade]tipo
Vamos pensar assim:
Preciso de uma lista de Gophers com capacidade 3.
Então, declaramos:
var array [3]Gopher
Atenção: Gopher é só um exemplo, logo vamos utilizar tipos reais.
Ficamos com o seguinte resultado:
Possuímos três índices do tipo Gopher, com valores nulos, ou seja, sabemos o tipo de cada índice, mas não sabemos o valor de cada tipo.
- “Mas como informo esses valores?
Cada valor será alocado em um índice, correto?
Dessa forma, podemos acessar o índice e atribuir um valor pra ele. O acesso fica assim:
var array [3]Gopherarray[0] = "Cinza"
array[1] = "Azul"
array[2] = "Rosa"
E nosso resultado final é:
CLARO COMO O DIA!
- “Ótimo, você utilizou um exemplo. No dia a dia, como devo aplicar um array?”
Declaração de Array
Vamos mudar o exemplo :
Gostaria de uma lista com o nome dos 5 maiores países do mundo.
Nossa declaração e atribuição deve ficar assim:
var paises [5]stringpaises[0] = "Rússia"
paises[1] = "Canadá"
paises[2] = "China"
paises[3] = "Estados Unidos"
paises[4] = "Brasil"fmt.Println(paises)//OUTPUT: [Rússia Canadá China Estados Unidos Brasil]
Note que inicializamos a variável paises
com capacidade igual a 5. Feito isso, preenchemos cada um de seus índices com valores do tipo string que representam os maiores países do mundo.
Quer melhor?
Também é possível inicializarmos a variável do nosso array com valores já definidos.
paises := [5]string{"Rússia", "Canadá", "China", "Estados Unidos", "Brasil"}fmt.Println(paises)//OUTPUT: [Rússia Canadá China Estados Unidos Brasil]
Entendemos a utilização básica de um array, portanto, podemos ver uma abordagem mais detalhada com o uso de matrizes.
Declaração de Matriz
Durante a utilização de nosso array
, também é possível criar e utilizar o conceito de matriz.
Antes que me pergunte:
De forma direta, uma matriz é um conjunto de valores distribuídos em linhas e colunas, que constituem uma tabela de valores multidimensionais.
- “Continuo não entendendo…”
Vamos chamar nossos queridos Gophers!
Pense em um jogo de de Gophers:
Possuímos diversos gophers, além do especial que ainda não foi desbloqueado.
Assim funcionam nossas matrizes, pois repare que cada gopher esta distribuído em um ponto de interseção entre uma linha e uma coluna.
Nossas linhas e colunas são representadas dentro da linguagem, cada uma, por um array
, ou seja, uma lista de valores diferentes, ou não, de tipos iguais.
- “Certo, mas como declaro isso?”
A declaração é bem simples:
var matriz [qtdLinhas][qtdColunas]tipo
Eu sei, ficou confuso. Vou explicar.
Usando a imagem anterior, repare que possuímos uma matriz 3x3, que possui 3 linhas e 3 colunas. Sabendo que cada linha e coluna é representado por um array
, tudo fica mais fácil, portanto, temos:
var matriz [3][3]Gopher
Atenção: Tipo Gopher é só um exemplo, logo vamos utilizar tipos reais.
Dessa forma, tudo começa a fazer sentido.
O processo de declaração de valores pode ocorrer de duas formas, explícito e implícito.
Declaração Explicita
Vamos pensar dessa forma:
Uma matriz 2x2 de inteiros.
Sua declaração fica:
var matriz[2][2]int
E sua definição de valores:
var matriz [2][2]intmatriz[0][0] = 1matriz[0][1] = 2matriz[1][0] = 3matriz[1][1] = 4fmt.Println(matriz)//OUTPUT: [ [1, 2] [3, 4] ]
Note que declaramos posição a posição.
Não ficou claro?
Nossa matriz é representada de acordo com o desenho acima
Agora sim!
Podemos deixar essa declaração ainda mais simples.
matriz := [2][2]int{[2]int{1, 2}, [2]int{3, 4}}fmt.Println(matriz)//OUTPUT: [ [1, 2] [3, 4] ]
- “Sempre vou ter que escrever o tipo?”
Uma das vantagens da linguagem esta na sua simplicidade, dessa forma, podemos falar das declarações implícitas.
Declaração Implícita
Utilizando o exemplo anterior, porém, com declarações implícitas, o resultado passa a ser:
matriz := [2][2]int{{1, 2}, {3, 4}}fmt.Println(matriz)//OUTPUT: [ [1, 2] [3, 4] ]
Simples, lindo e fácil!
- “Como posso consultar um valor?”
Lembra que cada valor está em uma posição específica de linha e coluna?
Através dessa informação encontramos e consultamos qualquer valor. Sua declaração fica:
matriz[linhaBusca][colunaBusca] //Valor posicional
Com valores, possuímos:
matriz := [2][2]int{{1, 2}, {3, 4}}fmt.Println(matriz[0][1])//OUTPUT: 2
Agora sim, entendemos todos os pontos relacionados a array
e matriz
.
Podemos falar de um parente próximo do array
, com uma pequena diferença, já que o mesmo é um pouco mais “liberal”. Estou falando do slice
.
2. Slice
O slice
pode ser, de forma indireta, chamado de “array flexível” já que diferente do array
convencional, não possui uma capacidade limite.
- “Liberdade total?”
Calma lá, não sejamos tão radicais.
COM GRANDES PODERES, VEM GRANDES RESPONSABILIDADES.
Primeiro vamos entender bem como funciona um slice
para que seja possível tirar o máximo de proveito dessa estrutura de dados.
Declaração
- A declaração de um
slice
é semelhante a de umarray
, a única diferença é que não definimos uma capacidade.
var slice []tipo{}
Note que quando inicializamos um slice
nulo precisamos adicionar um par de chaves {}
a fim de informar que ainda não existem item associados a ele. Tanto que, ao “printarmos” o valor do nosso slice
, o resultado apresentado é o de uma lista vazia.
slice := []tipo{}fmt.Println(slice)//OUTPUT: []
Até aqui, tudo lindo e maravilhoso!
O modo anterior de declaração, por sua vez não permite a declaração literal de um slice
, ou seja, caso você precise reservar um espaço inicial na memória para um número maior de elementos, precisamos utilizar a função make
.
Make
O make permite a inicialização de um slice
com tamanho definido de pré elementos e capacidade.
- Sua declaração é bem simples:
slice := make([]tipo, inicialização, capacidade)
Ao utilizarmos o make
, informamos que estamos inicializando um novo slice, com uma capacidade inicial já preenchida, podendo ou não ter uma capacidade final. Dessa forma, nosso sistema já reserva uma certa quantidade de memória na máquina.
- “Complicado…”
Vamos utilizar os Gophers para explicar.
Atenção: Tipo Gopher é só um exemplo, logo vamos utilizar tipos reais.
Repare que a declaração simples do slice
não deixa nenhum tipo de pré-registro na memória, ao contrário do slice
que foi inicializado com make
que definiu um espaço inicial de nove valores vazios do tipo Gopher.
- Dessa forma, ao “printarmos” nosso
slice
commake
, ficamos com:
slice := make([]int, 9)fmt.Println(slice)//OUTPUT: [0 0 0 0 0 0 0 0 0]
Agora nosso sistema já inicializou nove itens de valor “vazio” para o tipo int
.
- “Legal, agora entendi tudo. Só queria saber como faço para obter elementos especificos de um slice”
Assim como no array
, podemos obter um elementos utilizando o seu índice:
slice := []int{1, 2, 3}fmt.Println(slice[2])//OUTPUT: 3
Porém, também podemos obter uma faixa especifica de elementos, utilizando a seguinte sintaxe:
slice[de:até]
- de - Índice inicial da busca;
- até - Índice final, mas não incluso da busca.
Para não enrolarmos mais:
slice := []int{1, 2, 3, 4, 5, 6, 7}fmt.Println(slice[1:4])//OUTPUT: [2 3 4]
Note que indicamos o índice 4 como final, mas o mesmo não foi incluído no resultado final, já que a linguagem exclui o índice de conclusão.
Agora que a base do slice
foi entendida, podemos falar de suas funções!
Então vamos falar das funções que podem ser utilizadas com slice
, seguindo essa ordem:
len
- Apresenta o tamanho doslice
;cap
- Apresenta a capacidade doslice
;append
- Permite adicionar novos itens aoslice
;copy
- Permite copiar elementos entreslice
;
Len
De forma direta, retorna o tamanho do nosso slice
.
- Slice
slice := []int{}fmt.Println(len(slice))//OUTPUT: 0
- Slice com make
slice := make([]int, 9, 15)fmt.Println(len(slice))//OUTPUT: 9
Cap
Retorna a capacidade momentânea do slice
.
- Slice
slice := []int{}fmt.Println(cap(slice))//OUTPUT: 0
- Slice com make
slice := make([]int, 9, 15)fmt.Println(cap(slice))//OUTPUT: 15
Append
Permite agrupar valores ou até mesmo um slice
dentro de um slice
.
- “Que confusão!”
GOPHERS!
A declaração de um append
é feita da seguinte forma:
append(sliceInicial, valorVinculado)
Nota: Quando utilizamos um append
, ele nos retorna um novo slice!
Sua utilização mais básica fica:
slice := []int{1,2,3}sliceNovo := []int{4,5,6}slice = append(slice, sliceNovo...)fmt.Println(slice)//OUTPUT: [1 2 3 4 5 6]
- “Por que utilizou reticências (…) ?”
O operador ...
, chamado de spread
é uma forma de declararmos todos os elementos de um slice
de forma individual.
Pense no seguinte slice
:
sliceNovo := []int{1,2,3}
Agora, sabendo que um “print” dele ficaria assim:
//OUTPUT: [1 2 3]
Utilizamos o operador spread
para utilizar todos os itens como variáveis individuais, já que nosso append
não aceita um slice
como uma variável a ser acrescentada no nosso slice
inicial.
Sendo assim:
append(slice, sliceNovo...)
É exatamento o mesmo que:
append(slice, 1, 2, 3)
Copy
Assim como o append
, copy
faz a “transferência” de elementos de um slice
para outro.
A diferença é que o copy faz o processo de sobrescrita de valores entre posições de um slice
pai e um filho.
Sua declaração é feita da seguinte forma:
copy(sliceBase[de:ate], sliceDeCopia[de:ate])
Também algo bem simples!
slice := []int{1, 2, 3, 4, 5}sliceNovo := []int{8, 9, 10}copy(slice[0:2], sliceNovo[0:1])fmt.Println(slice)//OUTPUT: [8, 2, 3, 4, 5]
Note que informamos que o primeiro elemento do slice
deveria ser substituído pelo primeiro elemento do sliceNovo
.
Antes que me pergunte, vou abordar algumas diferenças entre o append
e copy
Append X Copy
Antes de apresentar detalhes em código, vamos descrever de forma simples, a lógica aplicada em cada um.
- Append
Caso o capacidade de um slice
seja atingida, o mesmo se encarrega de amplia-la.
slice := make([]int, 3, 5)novoSlice := []int{1, 2, 3, 4, 6, 7}slice = append(slice, novoSlice...)fmt.Println(slice)fmt.Println(cap(slice))fmt.Println(len(slice))//OUTPUT:
[0 0 0 1 2 3 4 6 7]
10
9
Sim, você não viu errado!
Mesmo declarando que a capacidade do nosso slice
é 5, após preenchermos todos os “espaços” pré estabelecidos, a própria linguagem se responsabilizou de liberar mais espaço de forma exponencial.
Vendo que a capacidade de 5 foi atingida, automaticamente a mesma foi ampliada para 10, comportando-se assim sempre que for necessário.
- Copy
De forma alguma o copy
altera a capacidade de um slice
.
slice := make([]int, 3, 3)novoSlice := []int{1, 2, 3, 4, 6, 7}copy(slice, novoSlice)fmt.Println(slice)fmt.Println(cap(slice))fmt.Println(len(slice))//OUTPUT:
[1 2 3]
3
3
Note que, mesmo passando um slice
de capacidade maior, o copy sobrescreveu apenas a capacidade limite da variável slice
por elementos da variável novoSlice
.
Enfim chegamos ao fim do slice
e podemos abordar a nossa última estrutura de dados, o map
.
3. Map
O nosso “dicionário” das estruturas de dados. O map
diferente do array
e do slice
não se comporta por índices, mas sim pelo conceito chave/valor.
- “Já li muito até aqui, deixe isso mais claro!”
Pense que possuímos uma lista de produtos, e nela temos o nome do produto relacionado ao seu valor. Sendo assim, quando consultar um produto, logo, o retorno será o seu valor.
Considere a lista:
- Arroz (5.50)
- Feijão (4.40)
- Carne (15.30)
- Refrigerante (7.80)
Declaração
A declaração de um map é feita da seguinte forma:
var lista map[tipo]tipo
ou utilizando make
lista := make(map[tipo]tipo)
Note que declaramos duas vezes o tipo, já que um deles representa nossa chave, e o outro, nosso valor
Voltando a lista anterior, nossa map
inicializado fica assim:
listaProdutos := make(map[string]float32)
Seu preenchimento, segue o modelo:
lista[chave] = valor
Dessa forma, nossa lista de compras fica:
listaProdutos := make(map[string]float32)listaProdutos["Arroz"] = 5.50
listaProdutos["Feijao"] = 4.40
listaProdutos["Carne"] = 15.30
listaProdutos["Refrigerante"] = 7.80
Como estamos falando de Go, sua declaração pode ficar ainda mais simples!
listaProdutos := map[string]float32{"Arroz":5.50, "Feijao":4.40, "Carne": 15.30, "Refrigerante": 7.80}
- “E a consulta desses valores, como é feita?”
Como estamos utilizando o conceito chave/valor, podemos busca-los através da chave pré-definida.
listaProdutos := make(map[string]float32)listaProdutos["Arroz"] = 5.50
listaProdutos["Feijao"] = 4.40
listaProdutos["Carne"] = 15.30
listaProdutos["Refrigerante"] = 7.80fmt.Println(listaProdutos["Arroz"])//OUTPUT: 5.5
Além da declaração simples de um map
, podemos também trabalhar com conceitos de aninhamento, que trata-se de um map
dentro de outro.
Map Aninhado
Pense agora que queremos separar produtos por categorias:
- Casa : [ Sofá (100.00), Colchão (80.00), Mesa (100.00)]
- Cozinha: [ Faca (30.00), Colher(10.00), Garfo(10.00) ]
A declaração de um map
aninhado fica:
var aninhado map[tipo]map[tipo]tipo
Apenas adicionamos um map
como valor de resposta para outro map
.
Aplicando na lista anterior, ficamos com:
var itensCasa map[string]map[string]float32
Seu preenchimento, é semelhante ao map
comum, com a única diferença de possuir um nível a mais de declaração:
itensCasa := map[string]map[string]float32{ "Casa": { "Sofa": 100.00,
"Colchao": 80.00,
"Mesa": 100.00,
},
"Cozinha": { "Faca": 30.00,
"Colher": 10.00,
"Gargo": 10.00,
},}
Por fim, sua consultado ocorre por níveis:
itensCasa := map[string]map[string]float32{"Casa": {"Sofa": 100.00,
"Colchao": 80.00,
"Mesa": 100.00,
},
"Cozinha": {"Faca": 30.00,
"Colher": 10.00,
"Gargo": 10.00,
},}fmt.Println(itensCasa["Casa"]["Sofa"])//OUTPUT: 100
Para finalizarmos o assunto de map
, precisamos falar de sua capacidade de exclusão de chaves.
Exclusão
Para que seja possível excluir uma chave de um map
, utilizamos a seguinte estrutura:
delete(map, chave)
Logo:
listaProdutos := make(map[string]float32)listaProdutos["Arroz"] = 5.50
listaProdutos["Feijao"] = 4.40delete(listaProdutos, "Feijao")fmt.Println(listaProdutos)//OUTPUT: map[Arroz:5.5]
Note que o item “Feijao” foi deletado do map final.
Conclusão
Chegamos ao fim de mais um tutorial da Gommunity.
ESSE FOI MUITO LONGO!
Espero que a utilização das estruturas de dados tenha ficado bem claro, para que você possa tirar o máximo de proveito dessa prática no seu dia a dia, pessoal e profissional.
Nesse tutorial vimos:
array
;slice
;map
Esse tutorial gigantesco merece um um aplausozinho? hahahaha
Nos vemos no próximo tutorial sobre Funções!