Fundamentos
1. Algumas definições importantes
Section titled “1. Algumas definições importantes”- Pode ser utilizado para estudar com o playground: https://go.dev/play/
- Link para documentação: https://go.dev/doc/effective_go
- Go foi criado com os três pilares: efficient compilation, efficient execution and ease of programming.
2. Primeiro programa em Go
Section titled “2. Primeiro programa em Go”// Line comments - Hello World from GoLangpackage main
import "fmt"
func main() { fmt.Println("Hello, Mundo!")}/*Bloco de comentários!Aqui várias linhas */Quando compartilhando código para foruns, é interessante enviar o código da pergunta utilizando, por exemplo, o PlayGround. Ele permite que as pessoas editem e enviem sugestões de modificações para os autores.
Os programas para serem executados, eles devem possuir um pacote main. Dentro deste pacote main, é necessário uma função main, ela é o ponto de entrada do nosso projeto.
Todos os códigos de Go são organizados em pacotes. É a forma como nossos projetos ficam distribuídos e organizados. O pacote fmt é da biblioteca padrão do Go, ele permite utilizar as funções de entrada e saída, por exemplo.
package main
import "fmt"
func main() { const nome = "Murilo Zanini" const idade = 36 const peso = 108.5 fmt.Println("Ola Mundo da Formatação!") fmt.Printf("Aqui vai uma string: %s\n", nome) fmt.Printf("Aqui vai um número inteiro: %d\n", idade) // Ultima linha de código da função não precisa do ; fmt.Printf("Aqui vai um número real: %.3f\n", peso)}Para ver mais detalhes de formatação: https://pkg.go.dev/fmt#Printf
A codificação UTF-8 é uma forma de armazenar os dados UNICODE dos caracteres de forma eficiente. Go utiliza UTF-8 e UNICODE para armazenar os dados. Para saber mais:
- https://developer.mozilla.org/pt-BR/docs/Glossary/UTF-8
- https://www.ime.usp.br/~pf/algoritmos/apend/unicode.html
- https://www.w3schools.com/charsets/ref_html_utf8.asp
Go é uma linguagem estáticamente tipada. Isso significa que os tipos são definidos no momento da compilação do nosso programa e não em sua execução. As variáveis para serem utilizadas precisam ser declaradas. Nesse ponto, a atribuição pode ser realizada de algumas formas:
- Atribuição com inicialização, não precisa definir o tipo da variável, ele é inferido de forma automática:
nome := "Murilo" - ATENÇÃO: Variáveis e constantes são inicializadas de forma distinta em Go.
- Para declarar uma variável e não atribuir um valor inicial para ela, é necessário utilizar o inicializador com valor zero:
var idade int. Desta forma, a variável está inicializada com um valor inicial. - Quando um valor é atribuído a uma variável e ela não é utilizada, o código lança um erro quando ele tenta ser executado.
package main
import "fmt"
func main() { // O ponto e virgula no final da expressa é opcional idade := 36 fmt.Printf("Valor da idade: %d\n", idade) var nome string fmt.Printf("Nome informado: %s\n", nome) // ATENÇÃO: Strings utilizam ", chars únicos utilizam ' nome = "Murilo" fmt.Printf("Nome depois da atribuição: %s\n", nome) // Multiplo inicializador e operador _ a, b, _, d := 1, 2, 3, 4 fmt.Printf("Valores informados: %d %d %d", a, b, d) // O operador _ é utilizado quando um valor de retorno é // fornecido e ele não será utilizado no contexto.}O nulo do Golang é nil.
Para apresentação de valores em binário e hexadecimal:
package main
import "fmt"
func main() { valor1, valor2 := 'a', 15 fmt.Printf("Valor em binário: %b\n", valor1) fmt.Printf("Valor em hexadecimal: %x", valor2)
}Para iniciar um valor além do zero inicial: var a int = 42. Para realizar o cast de tipo, utilizar:
package main
import "fmt"
func main() { var variavel1 int = 42 variavel2 := float32(variavel1) fmt.Printf("Valor em inteiro: %d\n", variavel1) fmt.Printf("Valor em float: %f\n", variavel2)
// Exibindo o tipo fmt.Printf("Tipo da variavel 1: %T\n", variavel1) fmt.Printf("Tipo da variavel 2: %T\n", variavel2)
}Para utilizar mais de um pacote, utilizamos o () para descrever todos os pacotes que estamos importando.
package main
// Doc da lib "math/rand" - https://pkg.go.dev/math/rand
import ( "fmt" "math/rand")
func main() { fmt.Printf("Valor aleatório 🎲: %d\n", rand.Intn(6))}IMPORTANTE: quando queremos que algo fique disponível fora do pacote que ele foi criado, utilizar letra maiúscula para descrever ela. Por exemplo, o valor MeuValor, fica disponível quando o pacote for importado, ela será exportada. Quando utilizamos o identificador meuValor, ele pode ser utilizado apenas dentro do pacote, ele não será exportado. Valor para funções. É equivalente ao conceito de público/privado de linguagens orientadas a objeto.
package main
import ( "fmt")
// Função não é exportada no pacotefunc encontraMaior(x, y int) int { if x > y { return x } return y}
// É possível retornar mais de um valor com uma funçãofunc trocados(valor1 int, valor2 int, valor3 float32) (float32, int, int) { return valor3, valor2, valor1}
func main() { x, y, z := trocados(10, 3, 4.) fmt.Printf("Chamda da função: %f\t%d\t%d\n", x, y, z)}Go suporte operações do tipo bitwise e bitshift. Para verificar mais sobre o que pode ser realizado com Go em nível de bit manipulation: https://pkg.go.dev/math/bits#pkg-overview.
Go possui um operador para trazer identificadores que são construídos com um incremento. Esse elemento é o iota. A documentação pode ser vista em: https://go.dev/wiki/Iota.
package main
import ( "fmt")
// Criando uma enumeraçãotype WeekDay int
const ( domingo WeekDay = iota //Fica valendo 0 segunda terca quarta quinta sexta sabado)
func main() { fmt.Printf("Utilizando valores do enum: %d\t%d\n", domingo, sabado)}Para executar um programa em Go:
go run ./nome_arquivo.goPara desenvolver os programas em Go utilizando o ambiente de desenvolvimento nativo, vamos precisar de algumas ferramentas:
- O compilador/runtime de Go
- Git instalado na máquina
- Um editor (no momento, o VS Code)
- Dependências de desenvolvimento (plugins do Go com VS Code)
Com a extensão de Go instalada no VS Code, utilizar o comando: go install para instalar as ferramentas e dependências de código. Selecionar todas e instalar. Todas as ferramentas são instaladas dentro do caminho indicado em $GOPATH.
Os módulos em GoLang são utilizados para realizar o namespacing das dependências do código. Desta forma é possível importar os módulos e realizar seu gerenciamento sem maiores problemas de conflito. Para iniciarmos um módulo, dentro do diretório da nossa solução, utilizar o comando: go mod init nome/modulo.
É possível compilar o programa para outras plataformas diferentes da de desenvolvimento utilizando Go. Verificar a documentação: https://go.dev/doc/tutorial/compile-install
O mod do Go é equivalente ao pip do Python. Para instalar as dependências listadas em um arquivo mod de um projeto, podemos utilizar o comando: go mod tidy. Mais sobre ele na documentação: https://go.dev/ref/mod .
Em GoLang, utilizamos o conceito de exportado ou não exportado para descrever o que está dentro do módulo/pacote ou apenas dentro dele.
Para importar um arquivo que está dentro do módulo, precisamos informar o caminho dele. Para isso, ele deve ter sua rota iniciada dentro do nome do pacote fornecido no comando de criar o pacote com o go mod.
Para um projeto com diversos diretórios, temos dentro do arquivo puppy.go:
package puppy
// Funções do pacotefunc Bark() string{ return "Au!"}
func Barks() string{ return "Ufufufufu!"}Agora dentro arquivo main.go:
package main
import ( "fmt" "modulos_projeto01/puppy")
func main() { fmt.Printf("%s\n", puppy.Bark()) fmt.Printf("%s\n", puppy.Barks())}O que estiver no pacote main não precisa estar em um diretório separado para sua execução. Para fazer um pacote utilizar dependências dos demais, editar o arquivo go.mod, para que ele possa ver os pacotes:
module modulos_projeto01
go 1.23.2Dentro do dog/funcoes.go:
package dog
import ( "strings")
func BigDog(msg string) (string){ return strings.ToUpper(msg)}Dentro do puppy/puppy.go:
package puppy
import ( "modulos_projeto01/dog")
// Funções do pacotefunc Bark() string{ return "Au!"}
func Barks() string{ return "Ufufufufu!"}
func BigBark() string{ return dog.BigDog(Bark())}E por fim, dentro da main.go:
package main
import ( "fmt" "modulos_projeto01/puppy")
func main() { fmt.Printf("%s\n", puppy.Bark()) fmt.Printf("%s\n", puppy.Barks()) fmt.Printf("%s\n", puppy.BigBark())}3. Estruturas de controle em Go
Section titled “3. Estruturas de controle em Go”Retomando: os programas em Go são iniciados no pacote main, pela função main.
Estrutura de decisão if similar a do C, Java. O bloco de código do if, deve estar entre {}. A condição de verificação não precisa estar entre (). Podemos encadear as estruturas de decisão utilizando else if. Os operadores lógicos são os mesmos de C e Java.
A estrutura switch pode ser utilizada de algumas maneiras distintas:
package main
import "fmt"
func main() { x := 10
// Exemplo de utilização de switch // Caso 1 switch { case x > 5: { fmt.Printf("Valor maior que 5\n") } case x < 5: { fmt.Printf("Aqui vai mais um!\n") } default: { fmt.Printf("Valor padrão\n") } }
// Caso 2 switch x { case 1: { fmt.Printf("O que fazer com o valor 1\n") } case 2: { fmt.Printf("Aqui para o 2\n") } }}Existe um operador chamado de select, ele funciona de forma similar ao switch, mas para o contexto de paralelismo e comunicação entre canais. Mais sobre o tema logo menos.
O loop for pode ser utilizado de algumas formas distintas:
package main
import "fmt"
func main(){ // Primeiro exemplo de utilização de for for x:= 0; x < 10; x++{ fmt.Printf("Primeiro Exemplo do For - %d\n", x) }
// Segundo exemplo de utilização de for x := 0 for x < 10 { fmt.Printf("Segundo Exemplo do For - %d\n", x) x++ }
// Terceiro exemplo de utilização do for x = 0 for{ fmt.Printf("Terceiro Exemplo do For - %d\n", x) x++ if x == 10 { break } }}É possível realizar interações uma dentro da outra (loops internos). Existe uma outra variação do for que permite utilizar ele para varrer um conjunto de valores dentro de um slice (similar a uma lista). Também pode ser utilizado com um mapa.
package main
import "fmt"
func main(){ // Criando um slice x := [] int{42, 34, 56, 78, 90, 12}
// Utilizando um for para interar por todos os valores for i, valor := range x{ fmt.Printf("Valor na posição %d \t %d\n", i, valor) }
// Utilizando a mesma lógica para navegar em mapas y := map[string]int{ "Murilo":36, "Goku":42, "Vegeta":44, }
for chave, valor := range y{ fmt.Printf("Valor declarado como chave: %s\t e o conteúdo: %d\n", chave, valor) }}Em muitas situações, é conveniente utilizar alguns tipos de dados que permitem trabalhar com diversos valores agregados. A linguagem Go traz alguns tipos agregados que permitem realizar esse tipo de manipulação. Essas estruturas são chamadas de aggregate data types.
Os tipos de dados agregados em Go são:
array: sequencia do mesmo tipo de dados. Não muda de tamanho. Em geral, utilizado nas implementações internas de Go.slice: construído sobre as características de um array, portanto só podem armazenar valores do mesmo tipo. Podem mudar seu tamanho. Possuem um comprimento e uma capacidade.map: armazenam um conjunto de chaves e valores, cada um de um tipo respectivo. Os valores não são armazenados de forma sequencia ou ordenada.struct: tipo composto de dados que permite representar um conjunto de diferentes variáveis.
4. Arrays
Section titled “4. Arrays”Exemplo de utilização de arrays:
package main
import "fmt"
func main(){ // Declara um array e realiza algumas manipulações com ele // Determinamos o nome do array, seu tamanho e seu tipo var meuArray [10]int
// Imprime o array fmt.Printf("%#v\t Tipo do arrat: %T\n", meuArray, meuArray)
// Para imprimir o array de forma não estruturada fmt.Printf("%v\n", meuArray)
// Atribuindo um valor para o array meuArray[0] = 10 meuArray[1] = meuArray[0]/3
// Imprimi o array novamente fmt.Printf("%v\n", meuArray)
// Declara e inicializa o array // Posições não iniciadas, tem seu valores zerado/nulo atribuído a elas. nomes := [5]string{"Murilo", "Goku", "Vegeta",}
// Declara o array e deixa o Go inferir a quantidade de memória necessária idades := [...]int{36,29,9}
// Imprime o array fmt.Printf("%#v\t Tipo do array: %T\n", nomes, nomes) fmt.Printf("%#v\t Tipo do array: %T\n", idades, idades)
// Utiliza o tamanho do array fmt.Printf("Tamanho do array: %d\n", len(idades))}5. Slices
Section titled “5. Slices”Slices são como arrays, mas eles podem ter seu tamanho alterado dinamicamente. Portanto mais elemento podem ser adicionados a ele. Os slices podem ser utilizados em conjunto com a função range também. Alguns detalhes de implementação dos slices:
package main
import ( "fmt" "strconv" //Para os casts de tipo)
// GOlang não suporta overloading de funçõesfunc MostrarSliceString(lista []string){ fmt.Printf("Conteúdo atual do Slice: %v\n", lista)}
func MostrarSliceInt(lista []int){ fmt.Printf("Conteúdo atual do Slice: %v\n", lista)}
func main(){
// Criando um slice // Aqui o slice é instânciado mas não tem nenhum valor atribuído a ele meu_slice := []string{}
MostrarSliceString(meu_slice)
// Adicionando elementos ao slice // Importante notar aqui: O append adiciona um novo elemento no slice, mas ele precisa realizar a reatribuição, que o retorno da função, caso contrário, ele não modifica o slice enviado para ele. _ = append(meu_slice, "entra aqui!")
MostrarSliceString(meu_slice)
// Com a reatribuição meu_slice = append(meu_slice, "Agora sim entra aqui")
MostrarSliceString(meu_slice)
// Instanciando e inicializando um slice novo_slice := []int{4, 5, 9, -78}
// Converter um inteiro para string e appenda no slice meu_slice = append(meu_slice, strconv.Itoa(novo_slice[0]))
MostrarSliceInt(novo_slice) MostrarSliceString(meu_slice)
// Utilizando o for-range for indice, valor := range novo_slice{ fmt.Printf("%d valor na posição %d\n", indice, valor) }}Ao tentar acessar um posição que não está dentro do range de um slice, um erro vai ser lançado. Partes de um slice podem ser acessadas utilizando os indices, como as listas em Python.
package main
import "fmt"
func imprimir(v []int) { fmt.Printf("%v\n", v)}
func algumaCoisa() { meu_slice := []int{4, 5, 8, 9} parte_1 := meu_slice[0:2] //4,5 parte_2 := meu_slice[:3] //4,5,8 parte_3 := meu_slice[2:] //8,9 imprimir(meu_slice) imprimir(parte_1) imprimir(parte_2) imprimir(parte_3)}
func main() { fmt.Println("Hello, 世界") algumaCoisa()}Importante, para remover um elemento do slice, fazemos uma cópia sem ele.
func RemoveIndex(s []int, index int) []int { return append(s[:index], s[index+1:]...)}Utilizando a função make(), é possível reservar memória para utilizar os elementos de um slice que serão conhecidos (espaço de memória que será utilizado). Esse espaço fica reservado, mas não altera o tamanho atual do slice.
// Pacote e código anterios
func verificandoSlice(lista []string){ fmt.Printf("Elemento recebido: %v\t Size: %d Capacidade: %d\n", lista, len(lista), cap(lista))}
func utilizandoMake(){ // Utilizando o make para alocar memória para um slice // O slice é iniciado com 0 elementos e reservando 10 posições de memória nomes := make([]string, 0, 10) verificandoSlice(nomes) nomes = append(nomes, "Murilo") nomes = append(nomes, "Vegeta") verificandoSlice(nomes) nomes = append(nomes, "Goku") verificandoSlice(nomes)}
func main(){ utilizandoMake()}É possível utilizar um slice de slices em Go. É como utilizar uma matriz de elementos.
func trabalhandoComSliceDeSlice(){ gigante := [][]int{} gigante = append(gigante, []int{1,2,3}) // Podem ser de tamanhos distintos gigante = append(gigante, []int{4,5,}) fmt.Println(gigante) fmt.Println("Exibindo apenas uma posição:", gigante[0][1]) fmt.Println("Exibindo número de linhas (quantos slices dentro):", len(gigante))}
func main(){ trabalhandoComSliceDeSlice()}Arrays, slices e outras estruturas em Go são enviados por valor. Para passar eles por referência, é necessário enviar eles utilizando um ponteiro.
6. Maps
Section titled “6. Maps”Os mapas são uma forma de construir estruturas do tipo chave-valor, com os mesmos tipos de dados em cada elemento da estrutura. Sua sintaxe é variavel := map[tipo_chave] tipo_valor{ chave:valor, chave2:valor2}. Alguns exemplos de manipulação de dicionários.
package main
import ( "fmt")
func Funcionalidade_01(){ // Cria o literal de um mapa idades := map[string]int{ "Murilo":36, "Vegeta":44, "Goku":42, }
// Exibe todo o mapa fmt.Println(idades)
// Altera o valor da idade de uma chave idades["Goku"] = 10 fmt.Println(idades)
// Inserindo valores nos mapas idades["Bulma"] = 35 fmt.Println(idades)
// Acessando todos os elementos dentro do mapa for chave, valor := range(idades){ string_saida := fmt.Sprintf("Valor %d com chave %s", valor, chave) fmt.Println(string_saida) }
// Testando se existe uma chave. Se o valor não for utilizado, um _ pode ser utilizado. valor, ok := idades["Blonko"] //Verifica se a chave existe if ok{ fmt.Println("Valor da chave:", valor) } else { fmt.Println("Essa chave não existe") }
// Para deletar uma chave // No caso de uma deleção de chave que não existe, nada é alterado no mapa. delete(idades, "Teste")
}
func main(){ Funcionalidade_01()}Verificar essa implementação:
package main
import ( "fmt")
func Funcionalidade_02(){ // Criando um mapa para listas comidas := map[string] []string{ "Murilo" : {"Lamen", "Macarrão", "Pizza",}, "Vegeta" : {"Hambuguer", "Doces"}, "Goku" : {"Lamen", "Hambuguer", "Hotdog",}, }
// Passando pelos elementos do mapa for chave, _ := range(comidas){ fmt.Printf("%s gosta de: ", chave) for _, dado := range(comidas[chave]) { fmt.Printf("%s\t", dado) } fmt.Println() }}
func main(){ Funcionalidade_02()}7. Structs
Section titled “7. Structs”Utilizando estruturas do tipo Struct, podemos colocar valores de tipos distintos no mesmo tipo abstrato de dado. As estruturas são muito semelhantes aos structs da linguagem C. Os elementos da struct podem ser acessados como os itens das estruturas em C, utilizando o operador ponto.
package main
import "fmt"
// Declaração das estruturas que são utilizadas no programatype person struct{ primeiroNome string sobreNome string idade int}
// A estrutura pode ser instânciada como uma variável
func TesteFuncionalidade01(){ p1 := person{ primeiroNome: "Murilo", sobreNome: "Carvalho", idade: 36, }
p2 := person{"Kakaroto", "Goku", 42}
p3 := person{ idade: 44, primeiroNome: "Vegeta", sobreNome: "Prince", }
// Exibe as três pessoas fmt.Println(p1) fmt.Println(p2) fmt.Println(p3)
// Acessa um elemento da struct fmt.Println("Nome de p1:", p1.primeiroNome)}
func main(){ TesteFuncionalidade01()}Structs podem ser utilizados dentro de outros structs (struct embedding). Neste caso, colocar os construtores (inicializadores) dentro da construção da estrutura mais externa (quando for necessário).
package main
import "fmt"
// Declaração das estruturas que são utilizadas no programatype person struct{ primeiroNome string sobreNome string idade int}
// Criando mais uma estruturatype professor struct{ pessoa person especialidade string}
func TesteFuncionalidade02(){ // Criando uma estrutura que possui outra estrutura dentro dela p1 := professor{ pessoa: person{ primeiroNome: "Murilo", sobreNome: "Carvalho", // Valores não inicializados, tem o valor 0 ou nil atribuídos a eles. }, especialidade: "Computação", }
fmt.Println(p1)}
func main(){ TesteFuncionalidade02()}É possível ter estruturas anônimas. Elas são definidas quando apenas os tipos da struct são definidos e os valores já são inicializados. Go também permite utilizar composição. Com as composições, os elementos internos podem ser acessados pelos elementos externos. Portanto, funções associadas aos tipos internos, estarão acessíveis para os elementos mais externos quando estes possuírem instâncias internas suas. Este comportamento da ao Go um mecanismo similar ao de herança e um mecanismo polimórfico a linguagem.
8. Funções
Section titled “8. Funções”Funções são uma forma de agrupar código. Isso terna o código desenvolvido mais simples de se reutilizar, compreender e manutenível. Este comportamento também traz maior capacidade de abstração para nosso código.
As funções possuem a seguinte estrutura básica: func (receiver) nomeDaFuncao (parametrosESeusTipos) tipoDeRetornoDaFuncao {codigoDaFuncao}. Um detalhe importante: definimos as funções descrevendo seus parâmetros. Utilizamos uma função informando seus argumentos.
IMPORTANTE: Tudo em Go é passado por VALOR. Uma função pode ter nenhum, um ou quantos tipo de retorno for necessário.
Uma função com o número de parâmetros variados é possível utilizando a notação de um parâmetro variático. Eles são enviados como um slice para a função. IMPORTANTE: um parâmetro variável deve ser o último declaro na função.
package main
import "fmt"
// Função com parâmetros variáveis, pode ser qualquer quantidadefunc adicionaValores(valores ...int) (int, int){ total := 0 for _, valor := range(valores){ total += valor } return total, len(valores)}
func main(){ // Chamando função adicionaValores fmt.Println(adicionaValores(1,2,3,4)) fmt.Println(adicionaValores(1,2,3,4,5,6,7,8,9)) // Enviando diversos valores, extraíndo todos eles valores := []int{3,4,5,6,7} fmt.Println(adicionaValores(valores...))}A extração de todos os elementos de um array/slice, é chamado de Unfurling. Todos os valores são extraído e enviados para uma função, por exemplo. IMPORTANTE: sem este operador, estamos enviando apenas um parâmetro. Com ele, estamos enviando a quantidade de elementos que o slice possuir de parâmetros.
O operador defer faz com que a chamada de uma função não seja resolvida no momento de sua realização. Desta forma, o sistema empilha sua chamada, resolve a função atual e quando ela termina, executa a função com defer.
package main
import "fmt"
func comportamento_02(){ defer ola1() ola2()}
// Para compreender a utilização do deferfunc ola1(){ fmt.Println("Ola 1")}
func ola2(){ fmt.Println("Ola 2")}
func main(){ comportamento_02()}Em geral, quando fazemos a alocação de algum recurso, como abrir uma conexão, um arquivo, utilizamos o defer para liberar este recurso.
É possível criar métodos em Go. Desta forma, as structs podem ter comportamentos associados a elas.
package main
import "fmt"
// Criando métodos em Gotype Pessoa struct{ nome string idade int}
func (p Pessoa) DescrevePessoa(){ fmt.Printf("Nome: %s\t Idade: %d\n", p.nome, p.idade)}
func comportamento_03(){ p1 := Pessoa{"Murilo", 36} p2 := Pessoa{"Jéssica", 29} p1.DescrevePessoa() p2.DescrevePessoa()}
func main(){ comportamento_03()}Em Go, interfaces declaram um conjunto de assinaturas para métodos. Desta forma, quem implementa aquelas interfaces, devem possuir uma implementação destes métodos. A capacidade polimórfica é a habilidade de um tipo realizar um comportamento de um outro tipo.
Em Go, os valores podem possuir mais de um tipo. Isso permite que realizamos a implementação do polimorfismo. Uma interface em Go é implementada utilozando a palavra interface, na frente do seu nome. Desta forma, implementamos a sobrecarga de funções utilizando o polimorfismo. Qualquer estrutura que implementar os métodos da interface, também vai ser do tipo da interface. CUIDADO: Observar o código a seguir com bastante cuidado para compreender o conceito apresentado.
package main
import "fmt"
// Criando métodos em Gotype Pessoa struct{ nome string idade int}
func (p Pessoa) DescrevePessoa(){ fmt.Printf("Nome: %s\t Idade: %d\n", p.nome, p.idade)}
type PessoaEspecial struct{ pessoa Pessoa altura float32}
// Método da estrutura PessoaEspecialfunc (p PessoaEspecial) DescrevePessoa(){ fmt.Printf("Altura: %.2f\t", p.altura) p.pessoa.DescrevePessoa()}
// Interface que traz uma assinatura. Todos que implementarem todos os seus métodos são deste tipo tambémtype Human interface{ DescrevePessoa()}
// Função para implementar o polimorfismofunc Descrever(h Human){ h.DescrevePessoa()}
func comportamento_04(){ p1 := Pessoa{"Murilo", 36} p2 := PessoaEspecial{Pessoa{"Jéssica", 29}, 1.49} Descrever(p1) Descrever(p2)}
func main(){ comportamento_04()}Existe uma interface chamada Stringer, que permite realizar o log de estruturas e elementos no código. Estruturas que possuem o método String() string implementa essa interface. Não precisa ser apenas uma estrutura, pode estar associado a uma variável, por exemplo.
package main
import ( "fmt" "strconv")
// Estudo da implementação da interface Stringer()
type Usuario struct{ nick string pass string nivel int}
// Cria uma implementação sobre um valortype Teste int
func (u Usuario) String() string{ return fmt.Sprintf("Usuario{nick:%s,pass:%s,elo:%d}", u.nick,u.pass,u.nivel)}
func (t Teste) String() string{ return fmt.Sprintf("Valor do Teste: %s", strconv.Itoa(int(t)))}
func main(){ user := Usuario{"Murilo","123456",5} fmt.Println(user) teste := Teste(10) fmt.Println(teste)}Em Go, existe um pacote padrão para lidar com logs da execução do programa. Ele é importado do pacote "log". Ele possui as funções impressão na saída padrão como o “fmt”.
Lembrando, podemos implementar uma enumeração no Go utilizando iota. Ela é implementada como uma sequencia de inteiros. Go permite utilizar Wrapper Functions, que são funções que encapsulam comportamentos. Desta forma, é possível direcionar um comportamento utilizando elas.
ATENÇÃO: Estudar o código a seguir com cuidado, tem vários detalhes importantes nele.
package main
import ( "fmt" "log")
// Define algumas estruturas para utilizartype User struct{ name string acessLevel AcessLevels}
// Cria uma enumeração quanto ao nível de acessotype AcessLevels intconst ( Visitante AcessLevels = iota Funcionario Administrador Dono)// Implementa a interface Stringer para utilizar a stringfunc (al AcessLevels) String() string{ // Utiliza o operador spread para encontrar o equivalente ao nível return [...]string{"Visitante", "Funcionario", "Administrador","Dono"}[al]}
func (user User) String() string{ return fmt.Sprintf("User{name:%s, acess: %s}", user.name, user.acessLevel)}
// Wrapper Function para o comportamento de Log// Está função recebe elementos que implementam a interface// Ela vai interceptar a chamada da função logfunc logInfo(s fmt.Stringer){ log.Println("LOG REALIZADO NA APLICAÇÃO: ", s)}
func main(){ u1 := User{"Murilo", Dono} u2 := User{"Vegeta", Administrador} u3 := User{"Goku", Funcionario}
log.Println(u1)
users := []User{u1,u2,u3} for _, user := range(users){ logInfo(user) }}9. Log e Escrita de Arquivos (Writter)
Section titled “9. Log e Escrita de Arquivos (Writter)”Primeiro vamos verificar como escrever um arquivo. Ele já está com algumas implementações futuras, mas ele é iniciado desta forma:
package main
// Pacote para realizar a interface de writerimport ( "io" "log" "os")
type Person struct{ name string}
// Interface de Writer, que recebe algo para ser escrito e retorna um erro se for preciso.func (p Person) writeOut (w io.Writer) error{ _, err := w.Write([]byte(p.name)) return err}
// Função para verificar se um erro aconteceufunc temError(e error) bool{ if e == nil{ return false } log.Fatalf("Erro: %s", e) // Nunca chega aqui return true}func main(){ // Cria um arquivo arquivo, err := os.Create("teste.txt")
// Testa para verificar se ocorreu um erro na criação do arquivo temError(err)
// Adicionar o encerramento do arquivo defer arquivo.Close()
// Cria um slice para enviar para o arquivo s := []byte("Ola Mundo!!")
// Escreve os bytes no arquivo // Atenção ao detalhes, nesse caso, a variável `err` já existe _, err = arquivo.Write(s)
temError(err)
}Uma string é um pouco diferente de um slice de bytes. É possível converter de um tipo para o outro. Em geral, os printer possuem um buffer. Isso é implementado para que um conjunto temporário possa acomodar um conjunto de valores antes de sua utilização.
package main
// Pacote para realizar a interface de writerimport ( "bytes" "io" "log" "os" "fmt")
type Person struct{ name string}
// Interface de Writer, que recebe algo para ser escrito e retorna um erro se for preciso.func (p Person) writeOut (w io.Writer) error{ _, err := w.Write([]byte(p.name)) return err}
// Função para verificar se um erro aconteceufunc temError(e error) bool{ if e == nil{ return false } log.Fatalf("Erro: %s", e) // Nunca chega aqui return true}func main(){ // Cria um arquivo arquivo, err := os.Create("teste.txt")
// Testa para verificar se ocorreu um erro na criação do arquivo temError(err)
// Adicionar o encerramento do arquivo defer arquivo.Close()
// Cria um buffer de bytes var buffer bytes.Buffer
// Cria uma pessoa p := Person{"Murilo"}
// Escreve o conteúdo de pessoa no buffer e no arquivo // Arquivo p.writeOut(arquivo) // No endereço do buffer - trabalha com ponteiros a função writeOut p.writeOut(&buffer)
// Escreve o conteúdo do buffer na tela fmt.Println(buffer.String())}Está é uma demonstração do uso de interfaces do Go.
10. Funções Anônimas
Section titled “10. Funções Anônimas”É um função que não possui um nome para invocação, apenas um comportamento e sua chamada.
package main
import "fmt"
// Função tradicionalfunc mostrar(s string){ fmt.Println(s)}
func main(){ // Chamando função tradicional mostrar("Murilo")
// Declarando e executando uma função anônima func(s string){ fmt.Println(s) }("Murilo")}Funções são cidadãos de primeira classe em Go, o que significa que eles podem ser considerados como um tipo. Portanto, podem ser atribuídos para variáveis. Elas também podem ser enviadas como parâmetros em outras funções.
package main
import "fmt"
// Função tradicionalfunc mostrar(s string){ fmt.Println(s)}
func main(){ // Chamando função tradicional mostrar("Murilo")
// Declarando e executando uma função anônima func(s string){ fmt.Println(s) }("Murilo")
// Atribuíndo uma função a uma variável x := mostrar
// Chamando a função pela variável x("Teste")
// Atribuindo uma função anônima y := func(x,y int) int { return x+y }
fmt.Println(y(3,4))}Quando retornar uma função, devolver apenas o nome da função, assim seu endereço será atribuído ao valor que invocou a função que a devolveu.
Em geral, funções enviadas como argumentos, estão enviando um callback para essa função. Em geral, permite que a função que recebeu esse argumento a execute quando for preciso.
Quando enviando uma função como callback, enviamos a assinatura da função no parâmetro.
package main
import "fmt"
// Definindo uma fun;cãofunc operacao(a int, b int, c func(int, int) int) int{ return c(a,b)}
func somar(a int, b int) int { return a+b}
func subtrair(a int, b int) int { return a-b}
func multiplicar(a int, b int) int { return a*b}
func dividir(a int, b int) int { return a/b}
func main(){ // Cria uma slice com as funções// Atenção: o tipo func() deve coincidir com as assinaturas das funçoes utilizadas. funcoes := []func(int,int)int {somar, subtrair, dividir, multiplicar,}
a := 10 b := 5
// Chamndo as diversas operações for _, funcao := range(funcoes){ fmt.Println(operacao(a,b,funcao)) }}Closures são funções dentro de funções. Elas são chamadas a cada chamada e execução da função mais externa. Ele mantém, no caso do exemplo abaixo, o valor da variável interna do Closure.
package main
import "fmt"
func TesteClosure() func() int{ x := 0 return func ()int{ x++ return x }}
func main(){ // Atribuí o closure para uma variável teste_closure := TesteClosure()
fmt.Println(teste_closure()) fmt.Println(teste_closure()) fmt.Println(teste_closure())}
// Saída: 1, 2, 3IMPORTANTE: Wrapper Functions
Section titled “IMPORTANTE: Wrapper Functions”Retomando: Wrapper Functions permitem trazer mais uma camada de abstração para outra função. Ela traz outras funcionalidades para uma função que já está implementada.
Importante: Quando utilizamos defer nas funções, elas são empilhadas em uma estrutura LIFO, portanto, a última função deferida, será a primeira resolvida.
11. Implementações de Testes com Go
Section titled “11. Implementações de Testes com Go”Os arquivos de teste em Go precisam seguir algumas convenções de nome para ser utilizados. Os arquivos de teste ficam no mesmo pacote, mas importam o módulo "testing". Quando vamos testar uma função, devemos criar o teste dela com outra função com o nome TestNomeFuncao(t * testing.T).
Arquivo main.go:
package main
func Somar(valores ...int) int { total := 0 for _,valor := range(valores){ total += valor } return total}
func main(){
}Arquivo main_test.go:
package main
import "testing"
func TestSomar(t *testing.T){ // Primeiro Teste valores := []int{4,6,7,8} total := Somar(valores...) if total != 25 { t.Errorf("Somatoria incorreta, esperado %d, recebido %d", 25, total) }
// Segundo Teste total = Somar(3,4,5) if total != 12{ t.Errorf("Somatoria incorreta, esperado %d, recebido %d", 12, total) }}Para executar o teste, rodar o comando: go test.
Observações importantes antes de seguir:
- O projeto deve estar em um módulo para rodar os testes. É importante que esse módulo não tenho o nome de
main, caso contrário não será possível importar o arquivo main.go para rodar os testes. - Para iniciar o módulo:
go mod init nomeDoModulo - Para rodar os testes:
go test, no diretório com os arquivos de teste, senão dar o endereço do diretório.
Para documentar um programa, devemos apenas colocar os comentários antes da função que vamos documentar. Iniciamos nossa documentação com o nome da Função/elemento que vamos documentar. A documentação completa pode ser gerada utilizando a ferramenta go doc.
ATENÇÃO: Precisa ser ajustado, não está implementado corretamente.
Section titled “ATENÇÃO: Precisa ser ajustado, não está implementado corretamente.”12. Ponteiros
Section titled “12. Ponteiros”Ponteiros permitem trabalhar com posições de memória em GoLang. O operador & nos da o endereço de uma variável. Os ponteiros em Go são muito semelhantes aos ponteiros em C. Lembrar que um ponteiro é um endereço de memória também.
package main
import "fmt"
// Estudo do uso de ponteirosfunc main(){ x:=42 fmt.Println(x) fmt.Println(&x) fmt.Printf("%v\t%T", &x,&x)}A saída para execução deste código:
420xc00000a0d80xc00000a0d8 *intUm * na frente de um tipo indica que é um ponteiro. No caso da saída produzida acima, estamos falando de um ponteiro para um número inteiro. O operador * é utilizado para derreferenciar uma posição de memória. Ele acessa o que está dentro do conteúdo da variável.
package main
import "fmt"
// Estudo do uso de ponteirosfunc main(){ x:=42 // Cria um ponteiro para x y := &x
// Mostra o valor de x fmt.Println(x) // Mostra o endereço de x, que é o valor armazenado em y fmt.Println(y) // Mostra o valor de x, por derreferenciar y fmt.Println(*y) // Mostra o endereço de x fmt.Println(&x) // Mostra o endereço de y fmt.Println(&y)}A saída do programa:
420xc00000a0d8420xc00000a0d80xc00004c050Podemos enviar endereços para funções, assim qualquer mudança realizada nela vai alterar os valores do ponto que fez a chamada.
package main
import "fmt"
// Função que recebe por valorfunc Dobro(x int){ x = 2*x}
// Função que recebe por refernciafunc DobroRef(x *int){ *x = 2 * (*x)}// Estudo do uso de ponteirosfunc main(){ x:=42
// Chama por valor Dobro(x) fmt.Println(x) // Chama por referencia DobroRef(&x) fmt.Println(x)}IMPORTANTE: Sempre que possível, trabalhar com passagem por valor, isso simplifica a utilização e depuração do funcionamento do programa. Pois torna mais claro os pontos que estão modificando o funcionamento do programa.
- Value Semantics: Passagem por valor. É o modo padrão de trabalho do GoLang.
- Pointer Semantics: Passagem de valor por referência. A função pode modificar o elemento dentro da função. Quando utilizamos grande quantidade de dados armazenados, pode ser uma abordagem interessante.
A função init() é executada antes da função main(). Em geral é utilizada para inicializar algum parâmetro ou elemento do nosso programa.
13. Tipos Genéricos
Section titled “13. Tipos Genéricos”São tipos de dados que podem assumir um conjunto de valores diferentes. Desta forma, quando diferentes tipos puderem ser aceitos em uma função, podemos utilizar este comportamento.
package main
import "fmt"
// Utilizando funções que não genéricasfunc SomaI(a,b int) int{ return a+b}
func SomaF(a,b float64) float64{ return a+b}
// Utilizando tipos genéricosfunc Soma [T int | float64](a,b T) T{ return a+b}
func main(){ i1 := 10 i2 := 20 f1:=32.0 f2 := 10.0 fmt.Println("Soma Inteiros:", SomaI(i1,i2)) fmt.Println("Soma Reais:", SomaF(f1,f2)) fmt.Println("Soma Inteiros:", Soma(i1,i2)) fmt.Println("Soma Reais:", Soma(f1,f2))}Este mesmo comportamento pode ser implementado utilizando interfaces.
package main
import "fmt"
// Utilizando funções que não genéricasfunc SomaI(a,b int) int{ return a+b}
func SomaF(a,b float64) float64{ return a+b}
// Utilizando tipos genéricostype MeusNumeros interface{ int | float64}
func Soma [T MeusNumeros](a,b T) T{ return a+b}
func main(){ i1 := 10 i2 := 20 f1:=32.0 f2 := 10.0 fmt.Println("Soma Inteiros:", SomaI(i1,i2)) fmt.Println("Soma Reais:", SomaF(f1,f2)) fmt.Println("Soma Inteiros:", Soma(i1,i2)) fmt.Println("Soma Reais:", Soma(f1,f2))}Quando utilizamos o operador ~ na frente de um tipo, estamos indicando que qualquer alias para aquele tipo serão consideradas como o tipo básico na utilização do tipo genérico.
14. Aplicações
Section titled “14. Aplicações”O gerenciamento de erros em Go é realizado, em geral, logo depois que um erro pode acontecer. Em Go não procuramos exceções, mas sim, verificamos se algum erro ocorreu no momento que ele poderia acontecer (retorno de uma função por exemplo).
IMPORTANTE: Uma string é um conjunto de bytes. Essa definição é muito importante, pois diversas funções utilizam um slice de bytes []bytes para funcionar.
Existe, em GoLang, duas formas de codificar algo em JSON, referentes a forma como é realizado o processo de mapear os tipos de dados de JSON para os primitivos de Go:
- Marshaling: Processo de transformar uma estrutura do Go (ou mapa com chaves como Strings) para JSON. IMPORTANTE: Os campos devem ser exportados (iniciar com letra maiuscula)
- Unmarshaling: Processo de trazer dados em JSON (slice de
[]bytes) para uma estrutura Go.
Para maiores informações, consultar a documentação aqui: https://go.dev/blog/json
package main
import ( "encoding/json" "fmt" "log")
// Criando as estruturas que serão utilizadas
type Person struct{ First string Last string Age int}
func (p Person) String() string{ return fmt.Sprintf("Person{\"first\":%s, \"last\":%s, \"age\":%d}", p.First, p.Last, p.Age)}
func main(){ p1 := Person{"Murilo", "Carvalho", 36} p2 := Person{"Vegeta", "Prince", 44} p3 := Person{"Kakaroto", "Goku", 42}
// Criando um JSON para enviar people := []Person{p1,p2,p3}
byteSlice,err := json.Marshal(people)
// Verifica se aconteceu algum erro no processo de codificação if err != nil { log.Fatalf("Erro ocorreu: %v", err) }
// Converte o byte slice para uma string fmt.Println(string(byteSlice))}Para verificar o processo de retirar os dados e colocar dentro das estruturas de dados:
package main
import ( "encoding/json" "fmt" "log")
// Criando as estruturas que serão utilizadas
type Person struct{ First string Last string Age int}
func (p Person) String() string{ return fmt.Sprintf("Person{\"first\":%s, \"last\":%s, \"age\":%d}", p.First, p.Last, p.Age)}
func main(){ p1 := Person{"Murilo", "Carvalho", 36} p2 := Person{"Vegeta", "Prince", 44} p3 := Person{"Kakaroto", "Goku", 42}
// Criando um JSON para enviar people := []Person{p1,p2,p3}
byteSlice,err := json.Marshal(people)
// Verifica se aconteceu algum erro no processo de codificação if err != nil { log.Fatalf("Erro ocorreu: %v", err) }
// Converte o byte slice para uma string fmt.Println(string(byteSlice))
// Convertendo agora um array de objetos // O `` é utilizado para string multilinhas s := `[{"First":"Murilo","Last":"Carvalho","Age":36},{"First":"Vegeta","Last":"Price","Age":44},{"First":"Kakaroto","Last":"Goku","Age":42}]`
bs := []byte(s)
// Cria um conjunto de dados para armazenar a resposta var saida []Person
err = json.Unmarshal(bs, &saida)
// Verifica se algum erro aconteceu if err != nil { log.Fatalf("Erro ocorreu: %v", err) }
fmt.Println(saida)
// Passando por cada um dos objetos for i, p := range(saida){ fmt.Println("Linha atual:", i) fmt.Println("Conteúdo:", p) fmt.Println("Nome:", p.First) }
}O pacote sort traz algumas ferramentas que podem ser utilizadas para ordenar conjuntos de dados primitivos e definidos pelo usuário. Mais informações na documentação do pacote: https://pkg.go.dev/sort#pkg-overview. Verificar o pacote bcrypt para criptografia de projetos.
15. Paralelismo e Concorrência
Section titled “15. Paralelismo e Concorrência”Go aproveita múltiplos cores do processador. Paralelismo acontece quando o código pode ser executado em núcleos diferentes, sendo executados ao mesmo tempo. Concorrência é um padrão de desenvolvimento. Quando existe recursos de hardware, código concorrente pode ser executado em paralelo. Verificar a palestra de Rob Pike (https://www.youtube.com/watch?v=oV9rvDllKEg).
Para iniciar a compreensão deste conceito, vamos verificar este programa:
package main
import "fmt"
func foo(){ for i := 0; i < 10; i++ { fmt.Println("Foo:", i) }}
func bar(){ for i := 0; i < 10; i++ { fmt.Println("Bar:", i) }}
func main(){ foo() bar()}Ele vai produzir a saída:
Foo: 0Foo: 1Foo: 2Foo: 3Foo: 4Foo: 5Foo: 6Foo: 7Foo: 8Foo: 9Bar: 0Bar: 1Bar: 2Bar: 3Bar: 4Bar: 5Bar: 6Bar: 7Bar: 8Bar: 9Como esperado, uma vez que a função foo() foi invocada antes da função bar(). É possível utilizando o pacote runtime medir algumas das características do sistema que roda nosso programa:
// Função para determinar a capacidade do nosso sistema operacionalfunc MedeAi(){ fmt.Println("OS:", runtime.GOOS) fmt.Println("ARCH:", runtime.GOARCH) fmt.Println("CPUs:", runtime.NumCPU()) fmt.Println("Goroutines:", runtime.NumGoroutine())}Quando desejamos utilizar uma Gorotine, que vai executar uma função de forma concorrente, utilizamos o operador go antes da chamada da função.
package main
import ( "fmt" "runtime")
func foo(){ for i := 0; i < 10; i++ { fmt.Println("Foo:", i) }}
func bar(){ for i := 0; i < 10; i++ { fmt.Println("Bar:", i) }}
// Função para determinar a capacidade do nosso sistema operacionalfunc MedeAi(){ fmt.Println("OS:", runtime.GOOS) fmt.Println("ARCH:", runtime.GOARCH) fmt.Println("CPUs:", runtime.NumCPU()) fmt.Println("Goroutines:", runtime.NumGoroutine())}
func main(){ MedeAi() go foo() go bar() MedeAi()}A saída do nosso programa será:
OS: windowsARCH: amd64CPUs: 6Goroutines: 1OS: windowsARCH: amd64CPUs: 6Goroutines: 3Repare que as saídas das funções não estão no nosso programa principal. Quando a função main() termina sua execução, todo o programa é encerrado. Existem algumas formas de sincronizar a execução das Gorotines.