Desenvolvimento Back-end Go

Principais estruturas de dados na Golang

Neste artigo iremos conhecer quais são, como podemos declarar e como utilizar as principais estruturas de dados na Golang.

há 1 ano 4 meses


Você sabia que a TreinaWeb é a mais completa escola para desenvolvedores do mercado?

O que você encontrará aqui na TreinaWeb?

Conheça os nossos cursos

A princípio, na Golang podemos utilizar diversos tipos de estruturas de dados. Cada uma dessas estruturas é mais bem indicada para resolver um determinado tipo de problema e podem ser úteis em diversas situações. As principais estruturas de dados na Golang são, arrays, slices e maps.

Assim sendo, neste artigo veremos quais as características de cada uma dessas estruturas de dados e como podemos utilizá-las em nossos programas em Go.

Arrays

Primeiramente vamos falar sobre o array, essa é a estrutura de dados mais básica da linguagem Go. Com ele podemos armazenar um número pré-definido de elementos do mesmo tipo de maneira ordenada.

Os elementos de uma array são identificados pela sua posição. Essa posição nós chamamos de índice e esse índice vária de 0 até o tamanho do array menos um.

Como declarar um array?

A princípio, para declarar um array precisamos informar o seu tamanho e qual o tipo dos dados que serão armazenados nesse array.

var array [<tamanho_do_array>]<tipo_do_array>

Então, para declarar um array com 10 posições que armazena valores do tipo int utilizamos a seguinte sintaxe:

var array [10]int

Além disso, também temos outras maneiras de declarar um array. Podemos realizar a declaração e a inicialização dos elementos de uma única vez. Da seguinte maneira:

array := [5]int{1, 2, 3, 4, 5}

Por exemplo, no código acima estamos declarando um array com cinco posições que armazena valores do tipo inteiro e já estamos preenchendo esse array com os valores dentro de chaves separados por vírgula.

Por fim, também podemos realizar a declaração e inicialização de elementos específicos em uma única linha.

var array [5]int{0: 1, 4: 5}

No código acima estamos declarando um array do tipo inteiro com cinco posições e inicializando a posição 0 com o valor 1 e a posição 4 com o valor 5.

Como acessar os elementos de um array?

Vamos para um exemplo prático, para criarmos um array que conterá dados do tipo string e irá possuir 10 posições fazemos da seguinte forma:

package main

func main() {
	var alunos [10]string
}

Uma vez que um array foi declarado, nós podemos acessar os seus elementos a partir do índice. Para acessarmos o valor contido na primeira posição do array utilizamos a seguinte sintaxe:

package main

import "fmt"

func main() {
	var alunos [10]string
	fmt.Println(alunos[0])
}

Quando o código acima for executado, será impresso no terminal apenas uma linha em branco. Pois ainda não definimos nenhum valor para as posições desse array e quando um array é declarado todas as suas posições são definidas como zero-value.

Para colocar algum valor em alguma posição do array, basta acessar o seu índice e então usar o operador de atribuição (=).

package main

import "fmt"

func main() {
	var alunos [10]string
	alunos[0] = "Cleyson"
	fmt.Println(alunos[0])
}

Uma vez que um array é declarado, o seu tamanho ou tipo não pode ser alterado. Logo, se tentarmos acessar um índice inexistente ou colocar um dado de um tipo diferente, teremos um erro.

package main

import "fmt"

func main() {
	var alunos [10]string
	alunos[10] = "Cleyson"
	fmt.Println(alunos[10])
}

Certamente o código acima irá causar um erro. Pois, estamos tentando acessar o índice 10 de um array de 10 posições, como os índices de uma array variam de 0 até o tamanho do array menos um, o array em questão possui um índice que varia de 0 até 9, logo, o índice 10 não existe.

package main

import "fmt"

func main() {
	var alunos [10]string
	alunos[0] = 10
	fmt.Println(alunos[0])
}

Analogamente o código acima também irá causar um erro, pois estamos atribuindo um valor do tipo int para um array que foi declarado que irá armazenar valores do tipo string.

Por fim, também é possível verificar o tamanho de um array com a função len, essa função vai nos retornar o tamanho de uma estrutura de dados, ela pode ser utilizada não só com arrays mas também com outras estruturas e até mesmo strings.

package main

import "fmt"

func main() {
	var alunos [10]string
	fmt.Println(len(alunos))
}

Slices

Já a slices na Go são estruturas de dados muito semelhantes aos arrays, elas podem armazenar dados de um mesmo tipo de maneira sequencial, a diferença é que slices possuem tamanho dinâmico, ou seja, elas não possuem um tamanho pré-definido no momento de sua criação e podem crescer de acordo com necessidade.

Por baixo dos panos as slices utilizam arrays, basicamente as slices são compostas por três partes, o primeiro é um ponteiro que aponta para um array utilizado para armazenar os dados atuais, o segundo é o tamanho atual do slices (a quantidade de elementos armazenados no slice) e o terceiro é a capacidade do array utilizado pelo slice.

Uma vez que o array atual do slice tenha atingido a sua capacidade máxima e seja necessário adicionar mais elementos, o que é feito por baixo dos panos são as seguintes etapas:

  1. é criado um array com o dobro da capacidade do array atual;
  2. os dados o array anterior são copiados para o novo array;
  3. o array anterior é apagado da memória;
  4. o slice passa a referenciar o novo array;
  5. as informações de tamanho e capacidade são atualizadas.

Como declarar um slice?

A forma de declaração de um slice é muito similar a forma de declaração de um array, a diferença é que não precisamos informar o seu tamanho.

var slice []int

Acima estamos declarando um slice que irá armazenar valores do tipo int, como esse slice foi apenas declarado e ainda não adicionamos nenhum elemento dentro do mesmo, ele possui um tamanho de 0 e uma capacidade de 0. Podemos utilizar a função len para verificar o seu tamanho e a função cap para verificar a sua capacidade.

package main

import "fmt"

func main() {
	var slice []int
	fmt.Println(len(slice), cap(slice))
}

Também é possível criar um slice e já popular esse slice com elementos iniciais, de forma bem similar como fazemos com os arrays.

slice := []int{1, 2, 3, 4, 5}

Nesse caso estamos declarando um slice de inteiros e então inicializando esse slices com os valores que estão dentro de chaves separados por vírgula. Nesse caso os slice terá um tamanho de 5 e uma capacidade de 5.

package main

import "fmt"

func main() {
	slice := []int{1, 2, 3, 4, 5}
	fmt.Println(len(slice), cap(slice))
}

E por fim, podemos declarar um slice já um tamanho e capacidade inicial, mas sem inicializar os seus elementos, para isso, utilizamos a função make.

slice := make([]int, 5)

No exemplo acima, estamos utilizando a função make para criar um slice de inteiros já com uma capacidade e tamanho de 5.

package main

import "fmt"

func main() {
	slice := make([]int, 5)
	fmt.Println(len(slice), cap(slice))
}

Como acessar os elementos de um slice?

Para acessarmos os elementos de um slice usamos os índices, assim como fazemos com um array, ou seja, caso queiramos acessar o segundo elemento de um slices utilizamos a sintaxe slice[1].

package main

import "fmt"

func main() {
	slice := []int{1, 2, 3, 4, 5}
	fmt.Println(slice[0])
	slice[0] = 10
	fmt.Println(slice[0])
}

Veja que também é possível alterar o valor de alguma posição de um slice da mesma maneira como fazemos com um array utilizando o operador =.

Adicionando elementos em um slice

Agora que já sabemos o que são, como declarar e como acessar os elementos de um slice. Vamos aprender como adicionar elementos dentro de um slice.

package main

import "fmt"

func main() {
	slice := []int{}
	fmt.Println(len(slice), cap(slice))
	slice[0] = 10
	fmt.Println(slice[0])
}

Nesse exemplo, foi criado um slice vazio, ou seja, possui um tamanho de zero e uma capacidade de zero, logo em seguida fizemos a atribuição do valor 10 para a posição 0 do nosso slice e por fim realizar a exibição dessa mesma posição do slice no terminal. Ao executar esse código iremos obter o seguinte resultado:

0 0
panic: runtime error: index out of range [0] with length 0

goroutine 1 [running]:
main.main()
        /home/cleysonph/treinaweb/artigos/estruturas_dados_go/main.go:8 +0x7a
exit status 2

Isso significa que obtivemos um erro ao executar o nosso programa, veja o compilador do Go informa que o seguinte erro aconteceu: panic: runtime error: index out of range [0] with length 0, basicamente essa mensagem fiz que tentamos acessar uma posição inexistente do nosso slice. Isso porque o nosso slice possui tamanho zero e capacidade zero, logo não é possível atribuir nenhum valor e também não é possível acessar nenhum valor pelo índice.

A função append

Então como podemos adicionar novos valores dentro de um slice? Bem, a maneira correta de adicionarmos novos valores em uma slice é através da função append, basicamente essa função irá criar um slice, com uma maior capacidade adicionando um novo elemento ao final desse slice.

package main

import "fmt"

func main() {
	slice := []int{}
	fmt.Println(len(slice), cap(slice))
	slice = append(slice, 1)
	fmt.Println(len(slice), cap(slice))
	fmt.Println(slice[0])
}

Veja como feito o uso da função append, nós atualizamos o valor da variável slice com o retorno da função append, além de passarmos para a função o slice atual e novo valor a ser adicionado no slice. Ao executar esse código veremos o seguinte resultado no terminal:

0 0
1 1
1

Inicialmente foi impresso o valor 0 0, pois o slice foi criado sem nenhum elemento, ou seja, com tamanho zero e capacidade zero, logo em seguida temos a impressão do valor 1 1, pois o tamanho e a capacidade do slice foi atualizada devido ao uso da função append e pôr fim a impressão do valor 1 que é justamente o elemento adicionado em nosso slice.

Lembrando que sempre que a capacidade de um slice é dobrada quando a capacidade atual é atingida. Veja o exemplo abaixo:

package main

import "fmt"

func main() {
	slice := []int{}
	fmt.Println("Tamanho:", len(slice), "Capacidade:", cap(slice))

	slice = append(slice, 1)
	fmt.Println("Tamanho:", len(slice), "Capacidade:", cap(slice))

	slice = append(slice, 2)
	fmt.Println("Tamanho:", len(slice), "Capacidade:", cap(slice))

	slice = append(slice, 3)
	fmt.Println("Tamanho:", len(slice), "Capacidade:", cap(slice))
}

Ao executar esse código, veremos o seguinte resultado no terminal:

Tamanho: 0 Capacidade: 0
Tamanho: 1 Capacidade: 1
Tamanho: 2 Capacidade: 2
Tamanho: 3 Capacidade: 4

Primeiramente temos a impressão do valor Tamanho: 0 Capacidade: 0, pois o slice acabou de ser criado, então sua capacidade e tamanho inicial são zero.

Logo em seguida, temos o valor Tamanho: 1 Capacidade: 1, pois foi realizado o primeiro append nesse slice. Como o dobro de zero é zero, então a capacidade é aumentada em uma unidade.

Em seguida, temos a impressão do valor Tamanho: 2 Capacidade: 2, pois realizamos mais um append. Como a capacidade já estava no seu limite, ela foi dobrada e o dobro de um é dois.

Por fim, temos a impressão do valor Tamanho: 3 Capacidade: 4, pois como fizemos mais um append a capacidade chegou ao seu limite mais uma vez e o dobro de dois são quatro.

Maps

Por fim, vamos falar dos maps, map é uma estrutura de dados associativa, isso quer dizer que os valores do map são associados com uma chave, algo semelhante à relação valor e índice que temos nos arrays e slices, a diferença nos maps é que definimos qual será o tipo e o valor dessas chaves.

Esse mesmo tipo de estrutura de dados é muito comum de aparecer em outras linguagens de programação com o nome de dicionário ou hash.

Como declarar maps?

Temos basicamente duas formas de criar um map, a primeira é utilizando a função make, e utilizamos a seguinte sintaxe make(map[]), como no exemplo abaixo:

package main

import "fmt"

func main() {
	map1 := make(map[string]int)
	fmt.Println(len(map1))
}

No exemplo acima criamos o map que utiliza o tipo string como chave e o tipo int como tipo dos valores associados a essas chaves. Também podemos utilizar a função len para verificar a quantidades de elementos que estão dentro do map, mas não é possível utilizar a função cap em um map.

Outra forma de declarar um map é utilizando uma declaração literal, muito semelhante como fazemos com arrays e slices, onde realizamos a declaração e inicialização dos elementos de uma única vez.

package main

import "fmt"

func main() {
	map1 := map[string]int{}
	fmt.Println(len(map1))
}

Basicamente utilizamos a sintaxe map[]{} para realizar a declaração literal, no exemplo acima não colocamos nada entre as chaves ({}), ou seja, criamos o map que possui chaves do tipo string e valores do tipo inteiro, mas que não possui nenhum elemento inicialmente.

Veja como poderíamos criar um map já com alguns valores iniciais:

package main

import "fmt"

func main() {
	aluno_nota := map[string]int{
		"João":  10,
		"Maria": 9,
		"Pedro": 8,
	}
	fmt.Println(len(aluno_nota))
}

Veja que os elementos são definidos dentro das chaves ({}) como a sintaxe chave : valor, e cada par de chave e valor é separado por vírgula.

Como acessar os elementos de um map?

Para acessar os elementos de um map utilizamos uma sintaxe bem semelhante a que utilizamos para realizar a acesso aos elementos de um array ou de um slice, a diferença é que ao invés de utilizarmos índices nós vamos utilizar a chave. Então a sintaxe para acessar os valores de um map é map[]. Veja o exemplo abaixo:

package main

import "fmt"

func main() {
	aluno_nota := map[string]int{
		"João":  10,
		"Maria": 9,
		"Pedro": 8,
	}
	fmt.Println(aluno_nota["João"])
}

Também é possível alterar o valor de alguma chave do map utilizando o operador =.

package main

import "fmt"

func main() {
	aluno_nota := map[string]int{
		"João":  10,
		"Maria": 9,
		"Pedro": 8,
	}
	fmt.Println(aluno_nota["João"])
	aluno_nota["João"] = 7
	fmt.Println(aluno_nota["João"])
}

E caso realizemos o acesso em uma chave inexistente iremos obter o zero-value do tipo definido na declaração do nosso map.

package main

import "fmt"

func main() {
	aluno_nota := map[string]int{
		"João":  10,
		"Maria": 9,
		"Pedro": 8,
	}
	fmt.Println(aluno_nota["Inexistente"])
}

No exemplo acima será impresso o valor 0, pois a chave Inexistente não existe em nosso map e como os valores são do tipo int o seu zero-value é 0.

Operações com maps

Temos três principais operações que podemos realizar com maps, seriam elas:

  • Adicionar novos pares de chave e valor;
  • Excluir pares de chave e valor;
  • Verificar a existência de um par chave e valor.

Adicionando elementos

Para adicionar novos pares de chave e valor em um map é bastante simples, basta realizar a atribuição para uma chave que ainda não existe no map. Veja o exemplo abaixo:

package main

import "fmt"

func main() {
	aluno_nota := map[string]int{
		"João":  10,
		"Maria": 9,
		"Pedro": 8,
	}
	fmt.Println("-------------------- Antes da adição --------------------")
	fmt.Println(aluno_nota)

	aluno_nota["Luiz"] = 7

	fmt.Println("-------------------- Depois da adição --------------------")
	fmt.Println(aluno_nota)
}

Ao executar o código acima veremos a seguinte saída no terminal:

-------------------- Antes da adição --------------------
map[João:10 Maria:9 Pedro:8]
-------------------- Depois da adição --------------------
map[João:10 Luiz:7 Maria:9 Pedro:8]

Veja que o par chave e valor Luiz:7 não existia antes de realizarmos o comando aluno_nota["Luiz"] = 7, pois como a chave Luiz não existia em nosso map, ao realizar a atribuição de um valor para essa chave ela foi adicionada.

Outro ponto interessante de observarmos é que a ordem dos elementos não foi mantida, veja que a chave Luiz:7 foi adicionada por último, mas ela ficou na segunda posição, isso acontece, pois o map não garante a ordem em que os elementos foram adicionados.

O motivo dos maps são guardarem a ordem dos elementos é porque os maps em Go são implementados como Hash Tables (Tabela de Dispersão), isso significa que ao adicionarmos um elemento em um map é executada uma função de hashing que irá definir um valor único a partir do elemento adicionado e isso permite que esses elementos sejam organizados de maneira mais performática.

Excluir elementos

Para realizarmos a exclusão de um par de chave e valor de um map utilizamos a função delete, ao executarmos essa função iremos informar no primeiro argumento qual o map que iremos modificar e como segundo elemento a chave que queremos excluir. Veja um exemplo:

package main

import "fmt"

func main() {
	aluno_nota := map[string]int{
		"João":  10,
		"Maria": 9,
		"Pedro": 8,
	}
	fmt.Println("-------------------- Antes da exclusão --------------------")
	fmt.Println(aluno_nota)

	delete(aluno_nota, "Pedro")

	fmt.Println("-------------------- Depois da exclusão --------------------")
	fmt.Println(aluno_nota)
}

A execução do código acima irá resultar na seguinte saída no terminal:

-------------------- Antes da exclusão --------------------
map[João:10 Maria:9 Pedro:8]
-------------------- Depois da exclusão --------------------
map[João:10 Maria:9]

Veja que a chave Pedro:8 foi excluída de nosso map. Caso passemos uma chave inexiste para a função delete não irá ocorrer nenhum erro e nenhum elemento será excluído.

Verificando a existência de um elemento

Para verificarmos a existência de uma chave dentro de um map também é algo bem simples. Veja o exemplo abaixo:

package main

import "fmt"

func main() {
	aluno_nota := map[string]int{
		"João":  10,
		"Maria": 9,
		"Pedro": 8,
	}
	valor, ok := aluno_nota["João"]
	fmt.Println(valor, ok)

	valor, ok = aluno_nota["Joãozinho"]
	fmt.Println(valor, ok)
}

A execução desse código terá o seguinte resultado:

10 true
0 false

O que acontece é que ao realizar o acesso a algum elemento de um map com a sintaxe map[] o que temos como retorno são dois valores, o primeiro é o valor correspondente a chave solicitada e o segundo é um booleano que informa se a chave solicitada foi ou não encontrada.

Caso lhe interesse apenas saber se a chave existe ou não, é possível ignorar o primeiro valor retornado utilizando um _ (underscore) para informarmos que desejamos ignorar o primeiro valor retornado.

package main

import "fmt"

func main() {
	aluno_nota := map[string]int{
		"João":  10,
		"Maria": 9,
		"Pedro": 8,
	}
	_, ok := aluno_nota["João"]
	fmt.Println(ok)

	_, ok = aluno_nota["Joãozinho"]
	fmt.Println(ok)
}

Conclusão

Com vimos nesse artigo, a linguagem de programação Go possui três principais estruturas de dados, sendo elas os arrays, slices e maps. Cada uma possui suas próprias características e são mais bem indicadas para resolução de problemas específicos.

Vimos com podemos realizar a declaração, acesso aos elementos e as principais operações em cada uma dessas estruturas de dados.

As estruturas de dados apresentadas são as mais básicas, fazem parte da própria linguagem e são as que você mais vai utilizar em seus programas, mas é importante frisar que existem outras estruturas de dados mais complexas disponíveis na biblioteca padrão da linguagem Go, como, por exemplo, o pacote container/list que disponibiliza uma implementação da estrutura de dados doubly linked list.

Por fim, caso queira aprender mais sobre a Go e sua infinidade de recursos saiba que aqui na TreinaWeb temos o curso Go Básico que possui 05h30 de vídeo e um total de 37 exercícios.

Veja quais são os tópicos abordados durante o curso Go Básico:

  • Compreender a sintaxe básica da Golang;
  • Compreender conceitos básicos envolvidos na Go, como ponteiros de memória;
  • Utilizar as estruturas básicas da linguagem, como declaração de variáveis;
  • Utilizar as principais estruturas de conjuntos da Go, como arrays, slices e maps;
  • Entender as principais funções built-in, como make(), new(), panic(), recover() e defer;
  • Organizar o código em pacotes e utilizar os principais pacotes disponibilizados pela linguagem;
  • Entender como podemos utilizar concorrência com a Golang, inclusive com técnicas como os channels;
  • Entender o que são as structs na Go e como podemos utilizar um pouco de orientação a objetos com a linguagem;
  • Realizar operações de I/O no sistema operacional, como a criação e escrita de arquivos.

Autor(a) do artigo

Cleyson Lima
Cleyson Lima

Professor, programador, fã de One Piece e finge saber cozinhar. Cleyson é graduando em Licenciatura em Informática pelo IFPI - Campus Teresina Zona Sul, nos anos de 2019 e 2020 esteve envolvido em vários projetos coordenados pela secretaria municipal de educação da cidade de Teresina, onde o foco era introduzir alunos da rede pública no mundo da programação e robótica. Hoje é instrutor dos cursos de Spring na TreinaWeb, mas diz que seu coração sempre pertencerá ao Python.

Todos os artigos

Artigos relacionados Ver todos