Introdução a Go

Luiz Couto
Kobe
Published in
11 min readDec 14, 2023

Este artigo é uma breve introdução prática sobre as principais características e aplicações da linguagem Go.

Imagem de vecstock — br.freepik.com

Go é uma linguagem de programação open source que teve seu desenvolvimento iniciado por engenheiros da Google em 2007. Foi lançada em 2009, mas foi a partir de anos recentes que se tornou popular e uma das principais linguagens adotadas para o desenvolvimento de diversas soluções de software, em especial aplicações backend e de alto desempenho. Segundo os criadores da linguagem, ela foi desenvolvida para aumentar a produtividade em uma nova era de máquinas com múltiplos processadores, de avanço das tecnologias de redes e de grandes bases de código.

Principais Características

Go é uma linguagem compilada e estaticamente tipada. Suas principais características são:

  • Simplicidade: Go foi desenvolvida para ser SIMPLES. Possui uma sintaxe que é fácil de entender e escrever.
  • Concorrência: Go já possui suporte nativo (built-in) extremamente eficaz e simples de usar para execuções paralelas.
  • Suporte multiplataforma: Go foi projetado para ser multiplataforma, o que significa que pode ser executado em vários sistemas operacionais, incluindo Windows, Linux e macOS.
  • Garbage Collector: Go possui garbage collector, que gerencia automaticamente a alocação e desalocação de memória, tornando mais fácil para os desenvolvedores escreverem código sem se preocupar com memory leaks. É claro, o garbage collector possui um custo computacional não descartável, e isso deve ser levado em consideração ao se construir programas em Go.

Go na prática

Para aprender mais sobre a linguagem, vamos iniciar um pequeno tutorial utilizando as principais ferramentas que Go disponibiliza para o desenvolvedor.

Instalando Go

Instalar Go em seu computador é bem simples, basta acessar https://golang.org/dl/ e seguir as instruções para fazer o download do instalador para seu sistema operacional.

Você pode utilizar o seu editor de texto favorito para escrever códigos em Go. Eu utilizo o Visual Studio Code.

Hello World!

Seguindo a tradição, vamos criar um programa em Go que imprima a string Hello World!. Para isso, crie um arquivo chamado main.go e insira o seguinte código:

package main

import "fmt"

func main() {
fmt.Println("Hello World!")
}

A primeira linha se refere ao nome do pacote, que no caso é main. Todo arquivo em Go deve obrigatoriamente começar com o nome do pacote. Pacotes são utilizados para organizar e reutilizar código.

A função main() no pacote main é necessária em um projeto em Go que se destina a ser construído como um programa executável. Esta função é o ponto de entrada do programa e é onde começa sua execução.

Note também que você importou um outro pacote no programa, o fmt. fmt é um pacote padrão de Go que possui tipos e funções de formatação e escrita de output em Go, semelhante ao printf em C.

Para executar o programa main.go, vamos primeiro fazer o build utilizando a ferramenta de CLI go com o comando build:

$ go build main.go

O comando build compila o programa passado como parâmetro, gerando um binário executável como resultado:

├── main.go
├── main

Portanto, podemos executar o binário e ver o output:

$ ./main
Hello World!

Alternativamente, podemos compilar e executar o programa em Go utilizando o comando run:

$ go run main.go
Hello World!

O comando run nada mais é do que o comando build seguido da execução do programa gerado.

Problema Prático

Vamos agora passar para um problema prático um pouco mais complexo. Nesse problema vamos modelar e definir diferentes pacotes, structs, interfaces e utilizar concorrência em Go com go routines e go channels. O problema é definido a seguir:

Um gato e um cachorro vivem em uma ilha. Mas eles são animais especiais: sabem fazer somas! Quando um humano chega na ilha, ele pede aos animais que somem uma lista de inteiros para ele. Os animais são inteligentes, e vão fazer de tudo para somar a lista de forma eficiente. Além disso, ele também pode chamar pelos animais, e cada animal emite um som/frase diferente.

Vamos modelar o problema acima e criar funções que somem a lista de inteiros de forma eficiente.

Iniciando o Projeto

Para iniciar um projeto em Go (conhecido como módulo), podemos utilizar o comando mod init presente na CLI de Go. Vamos dar o nome de go-math-island para nosso módulo:

$ go mod init go-math-island

Após a execução do comando, você verá que um novo arquivo será criado, chamado go.mod . Esse arquivo é responsável por gerenciar os pacotes e suas versões importados pelo seu módulo. Em analogia, é como se fosse o package.json em um projeto node.

Structs e Interfaces

Primeiramente vamos criar structs para representar o objeto Animal , assim como Cat e Dog. Uma Struct é um tipo que contém campos nomeados e tipados. Para isso, crie primeiramente uma pasta chamada animal. Nessa pasta estarão todos os arquivos do pacote animal. Além disso, crie um arquivo chamado animal.go dentro dessa pasta. A estrutura de pastas e arquivos ficaria como a mostrada a seguir:

├── animal
│ └── animal.go
├── main.go
├── go.mod

Então, insira o código abaixo em animal.go:

package animal

import "fmt"

type Animal struct {
name string
age int
isGoodAtMath bool
}

type Cat struct {
Animal
Color string
}

type Dog struct {
Animal
Kind string
}

A primeira linha se refere ao nome do pacote animal. Nesse pacote, definimos 3 structs ( Animal, Cat e Dog) . Repare que tanto Cat quanto Dog são também Animal, e utilizamos o que é chamado de embedding de structs em Go para que ambos herdem todas as propriedades do tipo Animal.

Em Go, quando escrevemos a primeira letra de um tipo em maiúsculo (como na struct Animal, na struct Cat, no campo de struct Color dentro de Cat…) estamos exportando aquele tipo para fora do pacote em que está definido. De modo similar, quando o tipo começa com a letra minúscula (como o campo name ou age ou isGoodAtMath dentro da struct Animal), aquele tipo não é exportado pelo pacote. Tipos exportados podem ser acessados fora do pacote. Portanto, para que um pacote exterior ao pacote animal (como o pacote main) possa construir uma struct do tipo Animal com todos os campos, vamos criar um construtor para ele:

...

func NewAnimal(name string, age int, isGoodAtMath bool) *Animal {
animal := Animal{
name: name,
age: age,
isGoodAtMath: isGoodAtMath,
}

return &animal
}

A função acima recebe alguns parâmetros e utiliza eles para retornar um ponteiro para uma instância do tipo Animal.

Em Go, podemos usar uma struct para criar métodos. Métodos nada mais são que funções atreladas à uma struct específica. Criar métodos facilita a legibilidade do código assim como cria um escopo para as funções. Vamos definir o método Sound para a struct Cat:

...

func (c Cat) Sound() string {
return "Meow!"
}

Agora, toda instância de Cat pode chamar a função Sound. Além disso, vamos também definir uma interface AnimalInterface:

...

type AnimalInterface interface {
TellMyName() string
DoMath(x int, y int) int
Sound() string
}

Em interfaces definimos um conjunto de métodos. Um conjunto de métodos é uma lista de métodos que um tipo deve ter para implementar a interface. Em resumo, Interfaces definem comportamentos.

Portanto, para que uma struct implemente a interface AnimalInterface, precisa implementar os métodos TellMyName(), DoMath(x int, y int) e Sound().

Vamos então criar mais dois métodos, dessa vez para a struct Animal:

...

func (a Animal) TellMyName() string {
return a.name
}

func (a Animal) DoMath(x int, y int) int {
return x + y
}

Perceba que como a struct Cat é um Animal e o método Sound() já é implementado para Cat, podemos dizer que Cat implementa a interface AnimalInterface. Vamos agora criar o método Sound para a struct Dog:

...

func (d Dog) Sound() string {
return fmt.Sprintf("Bark!")
}

Pronto! Agora Dog também implementa a interface AnimalInterface. Está na hora de testar o código feito até aqui. No arquivo main.go (fora da pasta animal), substitua o código do "Hello World!" pelo código abaixo:

package main

import (
"fmt"
"go-math-island/animal"
)

func main() {
cat := animal.Cat{
Animal: *animal.NewAnimal("Jiji", 3, true),
Color: "black",
}

dog := animal.Dog{
Animal: *animal.NewAnimal("Clifford", 8, true),
Kind: "labradoodle",
}

fmt.Println(WhatIsMyName(cat))
fmt.Println(WhatIsMyName(dog))
}

func WhatIsMyName(myanimal animal.AnimalInterface) string {
return myanimal.TellMyName()
}

Na função main() acima, criamos uma instância de Cat e outra de Dog. Utilizamos o construtor de Animal que criamos no pacote animal para popular ambas as structs, e também atribuímos valores aos campos particulares de cada uma delas ( Color para Cat e Kind para Dog ).

Perceba que utilizamos a desreferenciação (*) para obter o valor da variável armazenada no ponteiro da instância retornada pela função NewAnimal() . Fazemos isso porque a struct Cat e Dog esperam o tipo Animal , enquanto que a função NewAnimal() retorna um ponteiro para Animal ( *Animal ).

Além disso, foi criada uma função WhatIsMyName, que recebe a interface AnimalInterface como parâmetro. Desse modo, qualquer struct que implemente os métodos presente nessa interface poderá ser usada como parâmetro para a função. Ao executar o arquivo acima obtemos o resultado:

$ go run main.go
Jiji
Clifford

Agora vamos criar um pacote island, dentro de uma nova pasta chamada island, em que criaremos a função de soma de inteiros. Crie um arquivo island.go:

├── animal
│ └── animal.go
├── island
│ └── island.go
├── main.go
├── go.mod

Dentro desse novo arquivo, vamos definir a struct Island:

package island

import (
"go-math-island/animal"
)

type Island struct {
Cat *animal.Cat
Dog *animal.Dog
}

E então, vamos definir um método para esse struct que some uma lista de inteiros. Em Go, podemos definir os chamados arrays e slices para criar listas (equivalentes aos arrays de Javascript). Enquanto que arrays possuem tamanho fixo, os slices possuem tamanho variável. Definimos um array passando o tamanho dele dentro das chaves, enquanto que em slices não passamos o tamanho:

myArray := [3]int{}
mySlice := []int{}

Vamos então criar o método SumIntegersList:

...

func (i Island) SumIntegersList(intList []int) int {
sum := 0
for j := 0; j < len(intList); j++ {
sum = i.Dog.DoMath(sum, intList[j])
}
return sum
}

Nesse método, recebemos como parâmetro o slice de inteiros intListe retornamos a soma dessa lista. Utilizamos o método DoMath da struct Dog para realizar a soma dos números. Vamos então testar essa função. Substitua o código de main.go pelo código abaixo:

package main

import (
"fmt"
"go-math-island/animal"
"go-math-island/island"
)

func main() {

cat := animal.Cat{
Animal: *animal.NewAnimal("Jiji", 3, true),
Color: "black",
}

dog := animal.Dog{
Animal: *animal.NewAnimal("Clifford", 8, true),
Kind: "labradoodle",
}

islandService := island.Island{
Cat: &cat,
Dog: &dog,
}

intList := []int{}
for j := 1; j <= 1000000; j++ {
intList = append(intList, j)
}
fmt.Println(islandService.SumIntegersList(intList))
}

Criamos então a ilha e adicionamos o cachorro e o gato nela. Então, criamos uma lista dos inteiros de 1 a 1000000 e chamamos a função da ilha de soma de inteiros. Ao executar main.go, obtemos o resultado:

$ go run main.go
500000500000

Pronto! Esta é a soma dos inteiros de 1 a 1000000. Para ser mais eficiente, porém, poderíamos realizar a soma dessa lista de forma paralela! Podemos utilizar o gato também para ajudar na soma dos inteiros. Para isso, vamos utilizar go routines e go channels na próxima seção.

Go routines

Uma goroutine é uma função capaz de ser executada simultaneamente com outras funções. Para criar uma goroutine usamos a palavra-chave go seguida por uma invocação de função (simples assim). Go routines adicionam pouco overhead e podemos facilmente criar milhares delas. Vamos então criar uma nova versão do método SumIntegersList, porém agora utilizando Go routines:

...

func (i Island) SumIntegersListTogether(intList []int) int {
if len(intList)%2 == 1 {
intList = append(intList, 0)
}

numbersToEachAnimal := len(intList) / 2

totalSum := 0

go func() {
for j := 0; j < numbersToEachAnimal; j++ {
totalSum = i.Dog.DoMath(totalSum, intList[j])
}
}()

go func() {
for j := numbersToEachAnimal; j < len(intList); j++ {
totalSum = i.Cat.DoMath(totalSum, intList[j])
}
}()

return totalSum
}

O método acima possui a mesma assinatura que seu predecessor. Porém ele divide a tarefa da soma da lista de inteiros em duas. Uma primeira verificação é feita para que, caso a lista possua um número ímpar de elementos, adicionamos um elemento 0 para que possamos dividir a lista por dois sem obter restos. Depois disso, criamos duas go routines (que serão executadas paralelamente entre si e a função que as criou). Cada uma delas faz a soma de sua parte da lista (a parte de Cat e a parte de Dog). Depois, retornamos o valor da soma total. Antes de mais nada, vamos testar essa função e checar qual o valor obtido. Substitua a chamada da função da soma de inteiros em main.go pela nova função criada:

...
fmt.Println(islandService.SumIntegersListTogether(intList))
}

Ao executar main.go, obtemos o resultado:

$ go run main.go
0

0! Mas por que? A função não funcionou como esperado, mas vamos corrigi-la adiante! O resultado é 0 pois a execução da função SumIntegersListTogether é independente dos processos das go routines criadas por ela. Ou seja, ela retorna o valor da variável totalSum sem que espere pelo término das funções de cálculo da soma da lista.

Além disso, há outro problema grave com essa função: ela é vulnerável a race conditions. Isso porque a variável totalSum está sendo compartilhada entre 3 processos diferentes: o da função principal SumIntegersListTogether e os dois processos criados por ela (a soma feita pelo gato e pelo cachorro). Em programação paralela, evitar o compartilhamento de áreas de memória que podem potencialmente ser modificadas entre processos geralmente é uma boa prática. Um lema muito famoso em Go é:

“Não comunique compartilhando memória; ao invés disso, compartilhe memória por meio da comunicação.”

Vamos então utilizar go channels para resolver esses problemas.

Go channels

Go routines são executadas independentemente umas das outras. Elas podem se comunicar entre si através de estruturas conhecidas como channels. Você pode enviar valores para canais de uma goroutine e receber esses valores em outra goroutine.

Os channels se comportam como filas: todos os itens são recuperados na mesma ordem em que foram gravados (first in, first out — primeiro a entrar, primeiro a sair). Eles fornecem uma maneira para duas (ou mais) go routines comunicarem entre si e sincronizarem sua execução. Um tipo de um channel é representado pela palavra-chave chan seguida pelo tipo dos dados que são passados no canal.

Vamos ver como ficaria a função de soma da lista de inteiros paralela utilizando go channels:

...

func (i Island) SumIntegersListTogetherWithChannels(intList []int) int {
if len(intList)%2 == 1 {
intList = append(intList, 0)
}

numbersToEachAnimal := len(intList) / 2

sumChannel := make(chan int)

go func() {
sum := 0
for j := 0; j < numbersToEachAnimal; j++ {
sum = i.Dog.DoMath(sum, intList[j])
}
sumChannel <- sum
}()

go func() {
sum := 0
for j := numbersToEachAnimal; j < len(intList); j++ {
sum = i.Cat.DoMath(sum, intList[j])
}
sumChannel <- sum
}()

dogSum, catSum := <-sumChannel, <-sumChannel
totalSum := dogSum + catSum

return totalSum
}

Na nova função, é declarado a variável do tipo channel de inteiros, sumChannel. Esse canal será a única área de memória compartilhada entre as threads. Apesar de usarmos um tipo primitivo nesse exemplo (inteiro), em Go os channels podem ter qualquer tipo, incluindo structs e interfaces.

Para enviarmos um valor ao canal, utilizamos o nome do canal seguido pelo símbolo <- seguido pelo valor que queremos enviar naquele canal:

sumChannel <- sum

Para recebermos um valor do canal, usamos o símbolo <- seguido pelo nome do canal. O valor recebido pode ou não ser atribuído a uma nova variável.

dogSum := <-sumChannel

Após a finalização de cada um dos processos paralelos (da soma de cada uma das metades da lista), enviamos para o canal sumChannel o resultado. O processo principal (da função SumIntegersListTogetherWithChannels), por sua vez, pára sua execução para esperar pelos valores retornados pelos processos paralelos. Depois da soma concluída, é retornada a soma total. Vale ressaltar a simplicidade de se criar funções paralelas e também a simplicidade de sincronizar os diferentes processos.

Vamos então modificar a função main() em main.go para utilizar o novo método de soma paralela:

...
fmt.Println(islandService.SumIntegersListTogetherWithChannels(intList))

Ao executar o programa acima, obtemos o resultado:

$ go run main.go
500000500000

Que equivale a soma dos inteiros de 1 a 1000000! Dessa forma, conseguimos concluir a tarefa de somar paralelamente (juntando os esforços do gato e do cachorro da ilha) a lista de números inteiros. E, no caminho, passamos pelas principais características de Go e suas formas de utilização. Dessa forma, concluímos essa pequena introdução à linguagem Go. O código fonte desse tutorial pode ser acessado em https://github.com/luiz-couto/tutorial-go-math-island. Você pode alterar a branch no repositório para ver o código de cada um dos passos desse tutorial.

Se você chegou até aqui, parabéns! E muito obrigado ;)

--

--