#6 GoSchool: Estrutura de Dados e seus Métodos

Guilherme Caruso
Gommunity
Published in
12 min readNov 26, 2018

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?

L = linha; C = coluna

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 um array , 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 com make , 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 do slice ;
  • cap - Apresenta a capacidade do slice ;
  • append - Permite adicionar novos itens ao slice ;
  • copy - Permite copiar elementos entre slice ;

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 slicepor 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.80
fmt.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.40
delete(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!

--

--

Guilherme Caruso
Gommunity

Software Engineer | Gopher | Writer’s Padawan | INTP-T