Bem-vindos ao estudo da linguagem de programação Go (Golang)! Go é uma linguagem de programação moderna, eficiente e de código aberto criada pelo Google. Com uma sintaxe simples e poderosa, Go é projetada para facilitar o desenvolvimento de software escalável e concorrente.
- Sintexe simples de aprender, curva de aprendizado menor que outras linguagens.
- Oferece performance, concorrência e paralelismo.
- Reduz o consumo de poder computacional, disponibliza mecanismos que colobora exigir menos recursos.
- Plataforma com recursos nativos de segurança.
- Bastante utilizado para comunicação entre microservices via GRPC.
- Acesso simples aos recursos e protocolos modernos de rede.
- Nasceu otimizado para multi-core.
- Baixa utilização de memória, inicia uma thread utilizando apenas 2k.
- Garbage Collector otimizado.
- O Go é uma linguagem compilada, porém não utiliza máquina virtual.
- 2007: Início do projeto.
- 2009: Open Sourced.
- 2012: Versão estável v1.0.
- 2015: Melhorias importantes v1.5.
- 2018: Consolidação do ecossistema v1.11.
- 2022: Introdução do Generics v1.18.
- 2024: Novidades no HTTP Server para gerenciamento de rotas v1.22.
O Golang oferece retrocompatiiblidade com todas as versões anteriores, ou seja, uma versão mais atualualizada do Go, vai conseguir executar aplicações compiladas em qualquer versão anterior independente da evolução e do tempo.
O Go é uma linguagem fortemente tipada, porém, não é orientada a objetos e sim, orientada a dados.
- Programação.
- Testes
- Geração de documentos.
- Gerenciamento de dependências.
- Multiplataforma.
- Profilling.
- Security.
- Performance "out-of-box"
- Build & Deploy.
- Acesse go.dev, selecione o sistema operacional e sucesso.
- Instale o plugin oficial para VSCode Rich Go language support for Visual Studio Code.
Packages são como o Go organiza o código em um programa. Cada programa Go precisa de um pacote de entrada único. Um pacote Go não pode conter dois pacotes com nomes diferentes.
Geralmente, um programa Go começa pela função main, que é a entrada do programa. A partir da função main, é possível chamar outras funções ou pacotes para executar diversas tarefas no programa.
Link para pkg.go.dev que contém os pacotes padrão do Go
- Crie um arquivo main.go e escreva o código abaixo:
package main
import (
"fmt"
)
func main() {
fmt.Println("Olá Mundo")
}
- Comando para executar um progrma Go
go run main.go
No Go, não existem as visibilidades presentes no encapsulamento da orientação a objetos, a visibilidade é controlada pelo uso de letras maiúsculas ou minúsculas no início dos nomes.
Variáveis ou funções que começam com letra maiúscula são visíveis (exportadas), podendo ser acessadas por pacotes externos. Por exemplo, a variável Nome é exportada, enquanto a função calcularMedia não é. Veja:
package main
import (
"fmt"
)
// Variável exportada
var Nome string = "João"
// Função não exportada
func calcularMedia(n1, n2 float64) float64 {
return (n1 + n2) / 2
}
func main() {
fmt.Println(Nome) // Saída: João
// fmt.Println(calcularMedia(7.5, 8.2)) // Erro, função não exportada
}
Na função main, podemos imprimir a variável Nome, pois ela é exportada e visível em outros pacotes. No entanto, ao tentar chamar calcularMedia fora deste pacote, ocorrerá um erro, pois ela não é exportada e, portanto, não é visível em outros pacotes.
Variáveis são usadas para armazenar dados em um programa Go, já os tipos de dados são usados para especificar o tipo de dado que uma variável pode armazenar.
No Go, todo tipo de dado é fortemente tipado, e isso significa que cada variável deve ser declarada com um tipo de dado específico.
Toda variável no Go é iniciada com um valor padrão, exemplo:
- int = 0
- string = ""
- bool = false
- float64 = 0.0
Uma variável a nível de package é visível para todas as funções do pacote. Para declarar uma variável a nível de package, use a palavra-chave var
seguida do tipo de dado e do nome da variável.
Por exemplo, a seguinte declaração cria uma variável a nível de package chamada name
do tipo string
:
package main
import (
"fmt"
)
// declaração da variável name, que armazenará um texto
var name string
func main() {
// atribuição do valor "João" à variável name
name = "João"
// impressão do texto "Olá mundo! Meu nome é João." no console
fmt.Println("Olá mundo! Meu nome é", name, "!")
}
É possível declarar várias variáveis a nível de package de uma só vez, usando a seguinte sintaxe:
var (
name string
n1 int
n2 int
)
Vale observar que não é possível declarar variáveis com essa sintaxe no escopo de uma função.
Para atribuir um valor a uma variável a nível de função, use a seguinte sintaxe:
message := "Olá Mundo"
fmt.Println(message)
É possível atribuir valores a várias variáveis de uma só vez, usando a seguinte sintaxe:
var a, b, c = true, 2.3, "Olá"
fmt.Println(a, b, c)
Para trocar os valores de duas variáveis, use a seguinte sintaxe:
// declaração das variáveis x e y
var x, y = 10, 20
fmt.Println("Antes da troca:", x, y)
// troca os valores das variáveis x e y
x, y = y, x
fmt.Println("Depois da troca:", x, y)
As estruturas condicionais são usadas para controlar o fluxo de execução de um programa Go. No Go, existem dois tipos de estruturas condicionais: if
e switch
.
O condicional if é usado para verificar se uma condição é verdadeira ou falsa. Em go, o if não utiliza parentteses para delimitar a condição.
A sintaxe do if é a seguinte:
package main
import "fmt"
func main() {
a, b := 10, 15
// Comparando dois números inteiros
if a > b {
println("a é maior que b")
} else if a < b {
println("a é menor que b")
} else {
println("a é igual a b")
}
}
O condicional switch em Go permite avaliar várias condições e executar o bloco de código correspondente. Diferentemente de algumas linguagens, Go não requer break após cada caso. O exemplo abaixo ilustra isso:
package main
import "fmt"
func main() {
numero := 2
switch numero {
case 1:
fmt.Println("O número é 1")
case 2:
fmt.Println("O número é 2")
default:
fmt.Println("Número não reconhecido")
}
}
Neste exemplo simples de switch em Go:
numero é avaliado em cada caso:
- Se numero for 1, imprime "O número é 1".
- Se numero for 2, imprime "O número é 2".
- Se numero não corresponder a nenhum caso, o bloco default é executado, imprimindo "Número não reconhecido".
Não é necessário utilizar break após cada caso, pois o comportamento padrão do switch em Go é sair do switch após executar o caso correspondente.
Neste exemplo o código abaixo ilustra como usar um switch em vez de múltiplos if e else if:
package main
import "fmt"
func main() {
a, b := 10, 15
// Switch para comparar dois números inteiros
switch {
case a > b:
fmt.Println("a é maior que b")
case a < b:
fmt.Println("a é menor que b")
default:
fmt.Println("a é igual a b")
}
}
Loops são usados para repetir um bloco de código um determinado número de vezes. No Go, existe loop for
,porém diferente de outras linguagens, o Go não possui um loop while
.
Abaixo, segue a forma mais simples de se usar um loop for
em Go:
package main
func main() {
for i := 0; i < 10; i++ {
println(i)
}
}
O código abaixo demonstra três maneiras de iterar um array de strings:
package main
func main() {
names := []string{"Carlos", "Kelli", "Carol"}
for i := 0; i < len(names); i++ {
println(names[i])
}
for _, name := range names {
println(name)
}
for i, name := range names {
println(i, name)
}
}
- O primeiro método (for i := 0; i < len(names); i++) é tradicional e útil quando você precisa acessar os índices do array.
- O segundo método (for _, name := range names) é conveniente quando você só precisa dos valores do array e não dos índices.
- O terceiro método (for i, name := range names) é ótimo para quando você precisa tanto dos índices quanto dos valores do array.
No exemplo abaixo, usamos um loop for com uma condição simplificada. A variável i é declarada fora do loop e verificamos i < len(names) dentro do for. Em cada iteração, o elemento names[i] é impresso e i é incrementado. Isso oferece uma abordagem mais sucinta e fácil de entender:
package main
func main() {
names := []string{"Carlos", "Kelli", "Carol"}
var i int
for i < len(names) {
println(names[i])
i++
}
}
Este loop é útil quando queremos iterar sobre um array, mantendo o controle manual do índice. É uma alternativa clara e direta ao for tradicional com inicialização, condição e incremento.
O laço for {}
em Go é uma estrutura de loop infinito que é comumente usada em contextos de workers. Este tipo de loop é frequentemente utilizado para criar workers (trabalhadores) em Go, onde cada iteração do loop representa uma goroutine separada que executa uma determinada tarefa.
Por exemplo, em um sistema de processamento de filas, você pode ter um loop for {} que continua rodando para sempre. Dentro desse loop, você pode usar um canal para receber tarefas a serem executadas. Cada vez que uma tarefa é recebida do canal, uma nova goroutine é criada para processar essa tarefa.
A estrutura básica seria algo assim:
package main
import (
"fmt"
"time"
)
func worker() {
i := 1
// for infinito
for {
// Imprime a contagem atual
fmt.Println("Contagem infinita:", i)
i++
// Simula o processamento contínuo
time.Sleep(time.Second)
}
}
func main() {
fmt.Println("Iniciando worker...")
// Inicia o worker em uma goroutine
go worker()
fmt.Println("Worker iniciado.")
// Espera um pouco antes de encerrar o programa (para observar a contagem)
time.Sleep(time.Second * 10)
fmt.Println("Programa encerrado.")
}
Os tipos complexos em Go são estruturas de dados que podem armazenar múltiplos valores de tipos diferentes. São amplamente utilizados para modelar e organizar dados de maneira eficiente e flexível, oferecendo recursos poderosos para lidar com diferentes cenários de programação.
Alguns exemplos são:
- Array: Uma coleção fixa de elementos do mesmo tipo, com tamanho definido na declaração. Exemplo:
numeros := [5]int{1, 2, 3, 4, 5}
- Slice: Uma "fatia" dinâmica de um array, permitindo um tamanho flexível. Exemplo:
numeros := []int{1, 2, 3, 4, 5}
- Map: Uma coleção de pares chave-valor, onde cada chave é única. Exemplo:
pessoas := map[string]int{"João": 30, "Maria": 25, "Pedro": 28}
- Struct: Uma estrutura de dados que agrupa campos de diferentes tipos. Exemplo:
type Pessoa struct {
Nome string
Idade int
}
pessoa := Pessoa{"Ana", 35}
Em Go, um array
é uma estrutura de dados estática que representa uma coleção fixa de elementos do mesmo tipo. Ele é definido com um tamanho específico durante a declaração e não pode ser alterado após a criação.
package main
import "fmt"
func main() {
// Cria um array de tamanho fixo de strings
var nomes [3]string
// Atribui valores aos elementos do array
nomes[0] = "João"
nomes[1] = "Maria"
nomes[2] = "Pedro"
// Imprime o array
fmt.Println(nomes)
// Acessando elementos do array
fmt.Println(nomes[0]) // Imprime "João"
}
Neste exemplo, o array nomes é declarado com tamanho fixo de 3 elementos do tipo string. Os valores são atribuídos aos elementos individualmente. Como o array tem tamanho fixo, não podemos adicionar ou remover elementos após a sua criação. O acesso aos elementos é feito utilizando índices, como nomes[0] para acessar o primeiro elemento.
Resumo:
- Um array tem um tamanho fixo e é declarado com [Tamanho]Tipo.
- Os elementos de um array são acessados por índices.
- É estático e não pode ser alterado após a criação.
- Útil quando o tamanho é conhecido e não muda.
Em Go, um slice
slice é uma estrutura de dados dinâmica que representa uma fatia de um array. Ele possui um tamanho dinâmico e uma capacidade (ou tamanho do array subjacente), permitindo que cresça conforme necessário e pode ser considerado como tendo um tamanho "infinito" em termos de sua capacidade de expansão, pois o Go gerencia automaticamente a alocação de memória conforme mais elementos são adicionados. Isso permite que o slice cresça conforme necessário, sem a necessidade de especificar previamente seu tamanho máximo.
package main
import (
"fmt"
)
func main() {
// Cria um slice de strings
names := []string{"Tiago", "Daniel", "João"}
fmt.Println(names)
// Adiciona um novo elemento ao slice
names = append(names, "Pedro")
fmt.Println(names)
// Adiciona um novo elemento ao slice,
names = append(names, "João")
fmt.Println(names, len(names), cap(names))
}
Conforme o exemplo de código acima:
names := []string{"Tiago", "Daniel", "João"}
cria um slice de strings.append(names, "Pedro")
adiciona "Pedro" ao slice, automaticamente expandindo conforme necessário.append(names, "João")
adiciona "João" ao slice, mostrando sua capacidade dinâmica.len(names)
retorna o comprimento atual do slice (5 elementos).cap(names)
retorna a capacidade atual (8 elementos, expandindo conforme necessário).
Array:
- Um array em Go é uma coleção fixa de elementos do mesmo tipo.
- Seu tamanho é definido na declaração e não pode ser alterado.
- A declaração de um array seria assim:
var nomes [3]string // Array de strings com tamanho 3
nomes[0] = "João"
nomes[1] = "Maria"
nomes[2] = "Pedro"
Slice:
- Um slice em Go é uma "fatia" dinâmica de um array, permitindo um tamanho flexível.
- Não precisa de um tamanho definido na declaração.
- A declaração de um slice seria assim:
nomes := []string{"João", "Maria", "Pedro"} // Slice de strings
Em Go, um map
é uma estrutura de dados que mapeia chaves únicas para valores associados. Ele é semelhante a um dicionário em outras linguagens.
package main
import (
"fmt"
)
func main() {
// Create a map of names and ages
people := make(map[string]int32)
people["João"] = 45
people["Maria"] = 42
people["Antônio"] = 39
// Print the map
fmt.Println("People:", people)
// output
// People: map[João:45 Maria:42 Antônio:39]
// Get the age of Maria
name, ok := people["Maria"]
fmt.Println(name, ok)
// output
// Maria 42 true
// Try to get the age of Pedro
name, ok = people["Pedro"]
fmt.Println(name, ok)
// output
// 0 false
}
people := make(map[string]int32)
cria um map onde as chaves são strings e os valores são inteiros de 32 bits.people["João"] = 45, people["Maria"] = 42, people["Antônio"] = 39
adicionam valores ao map associados às chaves correspondentes.fmt.Println("People:", people)
imprime o map completo.- Saída:
People: map[João:45 Maria:42 Antônio:39]
- Saída:
name, ok := people["Maria"]
obtém o valor associado à chave "Maria" e verifica se a chave existe.- Saída:
Maria 42 true
- Saída:
name, ok = people["Pedro"]
tenta obter o valor associado à chave "Pedro", mas como essa chave não existe, o valor zero para o tipo (0 para int32) é retornado e ok é false.- Saída:
0 false
- Saída:
Em resumo, um map em Go é uma coleção de pares chave-valor onde as chaves são únicas e os valores podem ser recuperados rapidamente com base na chave. O uso do map permite uma busca eficiente e fácil acesso aos valores associados às chaves.
Em Go, uma struct
é uma coleção de campos que agrupa dados de diferentes tipos relacionados. A técnica para atributos públicos e privados em uma struct é determinada pela letra inicial do nome do campo:
package main
import "fmt"
type Client struct {
Name string
age int
Email string
Phone string
Status bool
}
func main() {
// Cria um novo cliente com os valores especificados
client := Client{
Name: "John Doe",
age: 30,
Email: "john.doe@example.com",
Phone: "123-456-7890",
Status: true, // O Go exige que sempre termine com vírgula
}
fmt.Println(client) // Imprime o cliente no console
fmt.Println(client.Name, client.Email) // Imprime o nome e o e-mail do cliente no console
}
type Client struct { ... }
define a estrutura Client com campos para informações de um cliente.client := Client{ ... }
cria um novo cliente com valores específicos para cada campo.fmt.Println(client)
imprime todos os campos do cliente no console.- Saída:
{John Doe 30 john.doe@example.com 123-456-7890 true}
- Saída:
fmt.Println(client.Name, client.Email)
imprime o nome e o e-mail do cliente.- Saída:
John Doe john.doe@example.com
- Saída:
Neste exemplo, client é uma instância da estrutura Client, que funciona como um "array" de campos com nome e tipo definidos. Cada campo pode ser acessado usando o ponto (.) seguido pelo nome do campo (client.Name, client.Email, etc.).
Trata-se da técnica de autorelacionamento por ponteiro, onde uma estrutura (struct) pode conter um campo que é um ponteiro para outra instância da mesma estrutura.
Isso permite criar relações hierárquicas dentro da mesma estrutura de dados, como um "pai" que pode ter um "filho" que também é do mesmo tipo. O campo Father na estrutura Client é um exemplo de autorelacionamento usando ponteiro.
package main
import "fmt"
type Client struct {
Name string
Age int
Email string
Phone string
Status bool
Father *Client // Autorelacionamento: Ponteiro para outro Client
}
func main() {
// Criando instâncias de Client
child := Client{
Name: "João",
Age: 30,
Email: "joao@example.com",
Phone: "123-456-7890",
Status: true,
}
father := Client{
Name: "Pedro",
Age: 25,
Email: "pedro@example.com",
Phone: "987-654-3210",
Status: true,
}
// Estabelecendo a relação "pai-filho"
child.Father = &father
// Imprimindo informações
fmt.Println("Cliente 1:")
fmt.Println("Nome:", child.Name)
fmt.Println("Idade:", child.Age)
fmt.Println("E-mail:", child.Email)
fmt.Println("Telefone:", child.Phone)
fmt.Println("Status:", child.Status)
if child.Father != nil {
fmt.Println("Pai:", child.Father.Name)
} else {
fmt.Println("Pai: Nenhum")
}
fmt.Println("\nCliente 2:")
fmt.Println("Nome:", father.Name)
fmt.Println("Idade:", father.Age)
fmt.Println("E-mail:", father.Email)
fmt.Println("Telefone:", father.Phone)
fmt.Println("Status:", father.Status)
}
Neste exemplo, criamos duas instâncias de Client
chamadas child
e father
. O child
é "João" e o father
é "Pedro". Em seguida, estabelecemos a relação "pai-filho" onde child aponta para father como seu "pai".
Ao imprimir as informações, podemos ver que child
tem um "pai" definido como father
, demonstrando o autorelacionamento usando um ponteiro para outra instância da mesma estrutura.
- Campos começando com letra maiúscula são públicos e podem ser acessados fora do pacote.
- Campos começando com letra minúscula são privados e só podem ser acessados dentro do mesmo pacote.
Conforme o exemplo de código acima, Name
é um campo público, permitindo acesso fora da struct, enquanto age
é privado e só pode ser acessado dentro do mesmo pacote. É uma técnica comum em Go para controlar a visibilidade e encapsulamento dos dados em uma struct.
Funções em Go são blocos de código independentes que podem ser chamados em qualquer parte do programa, e são definidas fora de qualquer estrutura ou tipo e podem ser chamadas com argumentos.
Para declarar uma função, use a seguinte sintaxe:
package main
import (
"fmt"
)
func hello(nome string) {
fmt.Println("Olá", nome, "!")
}
func sum(a, b int) int {
return a + b
}
func main() {
hello("João")
fmt.Println("Sua idade é:", sum(10, 20), "anos")
}
Em Go é comum usar funções com mais de um retorno, especialmente para lidar com possíveis erros. Por exemplo:
package main
import (
"errors"
"fmt"
)
// Função divide recebe dois inteiros e retorna um inteiro e um erro
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("divisão por zero")
}
return a / b, nil
}
// A função main é onde a execução do programa começa.
func main() {
// Chamando a função divide com 10 como dividendo e 2 como divisor
resultado, err := divide(10, 2)
if err != nil {
fmt.Println("Erro:", err)
} else {
fmt.Println("Resultado da divisão:", resultado)
}
// Chamando a função divide com 10 como dividendo e 0 como divisor
resultado, err = divide(10, 0)
if err != nil {
fmt.Println("Erro:", err)
} else {
fmt.Println("Resultado da divisão:", resultado)
}
}
Neste exemplo, a função divide tem dois retornos: resultado do tipo int e erro do tipo error. Se o divisor for zero, ela retorna um erro. No main, verificamos se há um erro após chamar a função divide e imprimimos a mensagem de erro. Caso contrário, imprimimos o resultado da divisão.
Métodos em Go são funções associadas a um tipo específico, chamado receptor (receiver). São definidos dentro do bloco de declaração de um tipo e têm acesso aos campos desse tipo. Métodos são chamados em uma instância (ou ponteiro) do tipo ao qual estão associados.
package main
import "fmt"
type Client struct {
Name string
Age int
}
// Método de Pessoa para exibir os detalhes
func (c Client) ShowDetails() {
fmt.Println("Nome:", c.Name)
fmt.Println("Idade:", c.Age)
}
func main() {
client := Client{Name: "Ana", Age: 30}
client.ShowDetails()
}
No exemplo acima, ShowDetails
é um método associado ao tipo cliente. Ele pode ser chamado em uma instância de Client
para exibir os detalhes dessa pessoa. Em resumo, os métodos são como funções específicas de um tipo, enquanto as funções em Go podem ser chamadas independentemente de qualquer tipo.
Em Go, métodos com ponteiro permitem modificar diretamente a estrutura original em que são chamados. Isso é particularmente útil ao lidar com estruturas de dados complexas ou quando queremos preservar as mudanças feitas nos métodos.
Vamos considerar a estrutura Category
e os métodos HasParent
e SetFather
:
package main
import "fmt"
type Category struct {
Name string
Parent *Category
}
func (c Category) HasParent() bool {
return c.Parent != nil
}
func (c *Category) SetFather(father *Category) {
c.Parent = father
}
func main() {
// Criando uma categoria "Notebooks" sem pai
notebooks := Category{Name: "Notebooks", Parent: nil}
// Verificando se "Notebooks" tem um pai (deve retornar false)
fmt.Println("Notebooks tem pai?", notebooks.HasParent())
// Criando uma categoria "Tecnologia" como pai de "Notebooks"
tecnologia := Category{Name: "Tecnologia", Parent: nil}
notebooks.SetFather(&tecnologia)
// Verificando se "Notebooks" tem um pai agora (deve retornar true)
fmt.Println("Notebooks tem pai?", notebooks.HasParent())
fmt.Println("Pai de Notebooks:", notebooks.Parent.Name) // Imprime "Tecnologia"
}
Neste exemplo, HasParent
verifica se uma categoria tem um pai, enquanto SetFather
atribui um pai a uma categoria. Ambos os métodos são definidos com um receptor de ponteiro (*Category), permitindo que modifiquem diretamente a estrutura Category original em que são chamados.
Em Go, não existe o conceito de herança da forma tradicional presente em algumas linguagens de programação orientada a objetos. Em vez disso, Go utiliza composição e interfaces para alcançar reutilização de código e polimorfismo.
- Em Go, é comum usar a composição para reutilizar funcionalidades de outros tipos.
- A composição permite que um tipo contenha outro tipo como um campo.
- Não há herança de membros de estrutura, mas sim incorporação (embedding) de tipos.
package main
import "fmt"
// Definindo um tipo Veiculo básico
type Veiculo struct {
Modelo string
Ano int
}
// Método para parar um veículo
func (v Veiculo) Parar() {
fmt.Printf("O veículo %s está parado.\n", v.Modelo)
}
// Definindo um tipo Carro que incorpora Veiculo
type Carro struct {
Veiculo // Incorporação do tipo Veiculo
Motor string
}
// Método para o carro andar
func (c Carro) Andar() {
fmt.Printf("O carro %s está andando com seu motor %s...\n", c.Modelo, c.Motor)
}
// Definindo um tipo Barco que incorpora Veiculo
type Barco struct {
Veiculo // Incorporação do tipo Veiculo
Helice string
}
// Método para o barco navegar
func (b Barco) Navegar() {
fmt.Printf("O barco %s está navegando com sua hélice %s...\n", b.Modelo, b.Helice)
}
func main() {
// Criando um Carro
carro := Carro{
Veiculo: Veiculo{Modelo: "Fusca", Ano: 1972},
Motor: "V8",
}
// Criando um Barco
barco := Barco{
Veiculo: Veiculo{Modelo: "Lancha", Ano: 2020},
Helice: "Marinha 3000",
}
// Chamando o método Andar para o Carro
carro.Andar()
// Chamando o método Navegar para o Barco
barco.Navegar()
// Chamando o método Parar para ambos os veículos
carro.Parar()
barco.Parar()
}
Neste exemplo, criamos os tipos Veiculo, Carro, e Barco, onde Carro e Barco herdam características do tipo Veiculo. Além disso, cada tipo possui atributos específicos que representam suas características únicas.
-
Veiculo
: É um tipo básico que representa as características comuns de um veículo, como o modelo e o ano. -
Carro
: É um tipo que incorpora Veiculo e também possui um atributo específico, Motor, que representa o motor do carro. -
Barco
: Também incorpora Veiculo e possui um atributo específico, Helice, que representa a hélice do barco. -
Os métodos
Andar
eNavegar
são específicos paraCarro
eBarco
, respectivamente. Eles demonstram a ação de andar para um carro e navegar para um barco, utilizando seus atributos específicos. -
O método
Parar
é comum a todos os tipos, pois um veículo de qualquer tipo pode ser parado.
No main
, criamos uma instância de Carro
e Barco
com seus respectivos atributos específicos (Motor
e Helice
), e demonstramos o uso dos métodos Andar
, Navegar
, e Parar
para cada veículo. Esta abordagem de herança e atributos específicos é bastante útil para representar objetos do mundo real de forma clara e eficiente em Go.
Em Go, interfaces são conjuntos de métodos que definem um comportamento. Assim como nas demais linguagens, uma interface define um contrato que especifica quais métodos uma struct deve implementar para satisfazer a interface. Isso permite que diferentes tipos possam ser tratados de maneira uniforme se implementarem os métodos da interface.
Um exemplo simples de uma interface em Go:
package main
import "fmt"
// Definindo a interface Animal
type Animal interface {
Som() string
Andar() string
}
// Definindo uma struct Dog que implementa a interface Animal
type Dog struct {
Nome string
}
// Implementação do método Som para Dog
func (d Dog) Som() string {
return "Au Au"
}
// Implementação do método Andar para Dog
func (d Dog) Andar() string {
return fmt.Sprintf("%s está caminhando...", d.Nome)
}
func main() {
// Criando uma instância de Dog
cachorro := Dog{Nome: "Rex"}
// Usando métodos da interface Animal
fmt.Println("Som do", cachorro.Nome+":", cachorro.Som())
fmt.Println("Movimento do", cachorro.Nome+":", cachorro.Andar())
}
Neste exemplo:
Animal
é uma interface que define dois métodos:Som
eAndar
.Dog
é uma struct que implementa a interfaceAnimal
ao definir os métodosSom
eAndar
.- No
main
, criamos um Dog chamadocachorro
e chamamos seus métodosSom
eAndar
, que são métodos da interfaceAnimal
.
Este exemplo demonstra como uma interface em Go permite que diferentes tipos (como Dog
, Cat
, Bird
, etc.) podem ser tratados de forma genérica se implementarem os métodos da interface. Isso proporciona flexibilidade e reutilização de código.
As asserções de interfaces em Go permitem verificar e converter um valor de interface para um tipo subjacente. É uma forma de verificar se uma interface contém um tipo específico e, se sim, obter o valor subjacente desse tipo. Isso é útil para lidar com tipos dinâmicos de forma segura.
package main
import "fmt"
// Definindo a interface Animal
type Animal interface {
Som() string
Andar() string
}
// Definindo uma struct Dog que implementa a interface Animal
type Dog struct {
Nome string
}
// Definindo uma struct Cat que implementa a interface Animal
type Cat struct {
Nome string
}
// Implementação do método Som para Dog
func (d Dog) Som() string {
return "Au Au"
}
// Implementação do método Andar para Dog
func (d Dog) Andar() string {
return fmt.Sprintf("%s está caminhando...", d.Nome)
}
// Implementação do método Som para Cat
func (c Cat) Som() string {
return "Miau Miau"
}
// Implementação do método Andar para Cat
func (c Cat) Andar() string {
return fmt.Sprintf("%s está caminhando...", c.Nome)
}
// Verifica o tipo subjacente da interface Animal
func show(a Animal) {
// Condicional switch com type para verificar o tipo subjacente da interface Animal passada como argumento
switch a.(type) {
case Dog:
fmt.Println("Som do", a.(Dog).Nome+":", a.(Dog).Som())
fmt.Println("Movimento do", a.(Dog).Nome+":", a.(Dog).Andar())
case Cat:
fmt.Println("Som do", a.(Cat).Nome+":", a.(Cat).Som())
fmt.Println("Movimento do", a.(Cat).Nome+":", a.(Cat).Andar())
default:
fmt.Println("Animal desconhecido.")
}
fmt.Println()
}
func main() {
// Criando uma instância de Dog
cachorro := Dog{Nome: "Rex"}
show(cachorro)
// Criando uma instÂncia de Cat
gato := Cat{Nome: "Bichano"}
show(gato)
}
Neste exemplo:
- No método
show
, usamos um switch comtype
para verificar o tipo subjacente da interfaceAnimal
passada como argumento. - Para cada caso (
Dog
ouCat
), realizamos a asserção de tipo(a.(Dog))
ou(a.(Cat))
para converter a interface para o tipo desejado. - Isso nos permite acessar os métodos específicos de
Dog
ouCat
(comoSom
eAndar
) e imprimir suas características. - Este método é útil quando temos uma interface e queremos executar comportamentos específicos com base no tipo subjacente da interface.
Ao trabalhar com Go, é importante compreender a diferença entre concorrência e paralelismo. Embora os termos possam parecer semelhantes, eles têm significados distintos no contexto da linguagem.
Em Go, a concorrência refere-se à capacidade de um programa executar várias tarefas ao mesmo tempo. Isso é alcançado usando goroutines, que são threads leves gerenciadas pelo Go runtime. Uma goroutine permite que você inicie uma função de forma independente e ela será executada de forma assíncrona em relação ao restante do programa. Isso é útil para processamento concorrente, como manipulação de requisições em servidores web ou execução de várias tarefas ao mesmo tempo.
Por outro lado, o paralelismo refere-se à capacidade de executar várias tarefas simultaneamente em CPUs múltiplos ou núcleos de CPU. Isso significa que várias partes do programa estão sendo executadas verdadeiramente ao mesmo tempo, proporcionando uma maneira de melhorar o desempenho ao lidar com tarefas intensivas em CPU.
O Go é conhecido por sua abordagem de concorrência em vez de paralelismo. Ele permite que você crie facilmente goroutines para executar tarefas de forma concorrente, aproveitando eficientemente o poder de processamento disponível.
Em resumo, o Go é projetado para ser concorrente, aproveitando as goroutines para realizar várias tarefas ao mesmo tempo em um único núcleo ou em vários núcleos.
As goroutines são uma forma de executar funções de forma assíncrona e concorrente, semelhante a threads, mas com um custo de criação muito menor.
- São "threads leves" gerenciadas pelo próprio runtime do Go.
- São unidades de execução leves que permitem que um programa execute funções de forma assíncrona e concorrente.
Podemos entender as goroutines como processos independentes que executam tarefas em segundo plano, permitindo que o programa continue executando outras operações.
- Leveza: Uma goroutine é leve em comparação com uma thread tradicional do sistema operacional. Isso significa que você pode criar milhares (ou até mesmo milhões) de goroutines em um programa Go sem consumir muitos recursos.
- Concorrência Simples: O Go facilita a criação e execução de goroutines. Basta adicionar a palavra-chave go antes de uma função para executá-la como uma goroutine.
- Comunicação: As goroutines podem se comunicar entre si usando canais (
channels
). Isso permite a sincronização e a troca de dados entre goroutines de forma segura.
package main
import (
"fmt"
"time"
)
func tarefa1() {
for i := 1; i <= 5; i++ {
fmt.Println("Executando tarefa 1...", i)
time.Sleep(time.Second) // Simula uma tarefa que leva 1 segundo
}
}
func tarefa2() {
for i := 1; i <= 3; i++ {
fmt.Println("Executando tarefa 2...", i)
time.Sleep(800 * time.Millisecond) // Simula uma tarefa que leva 800 milissegundos
}
}
func main() {
fmt.Println("Iniciando programa principal.")
// Executa a função tarefa1 como uma goroutine
go tarefa1()
// Executa a função tarefa2 como uma goroutine
go tarefa2()
fmt.Println("Continuando a execução do programa principal.")
// Aguarda um pouco para não terminar imediatamente
time.Sleep(4 * time.Second)
fmt.Println("Programa principal finalizado.")
}
Neste exemplo, tarefa1()
e tarefa2()
são executadas como goroutines simultaneamente. Enquanto uma está sendo executada, a outra também está em andamento, permitindo a execução concorrente das duas tarefas.
Vale lembrar que a função main
também é uma goroutine.
Channels em Go são uma poderosa ferramenta para a comunicação e sincronização entre goroutines. Eles fornecem uma maneira segura de transmitir dados de uma goroutine para outra.
- São canais de comunicação que conectam goroutines.
- Permitem que goroutines enviem e recebam valores de forma sincronizada.
- Seguem um modelo de comunicação baseado em "enviar" e "receber".
- Comunicação Segura: Channels garantem que a comunicação entre goroutines seja segura e sem race conditions (concorrência de dados).
- Blqueio: As operações de leitura (
<-
) e escrita (<-
) em um channel são bloqueantes. Isso significa que uma goroutine será bloqueada até que a outra esteja pronta para receber ou enviar dados. - Sincronização: Channels podem ser usados para sincronizar a execução de goroutines. Por exemplo, uma goroutine pode aguardar até receber um sinal de outra goroutine através de um channel.
ch := make(chan TipoDoDado)
ch <- valor
valor := <-ch
package main
import "fmt"
func enviarDados(ch chan<- int) {
for i := 1; i <= 5; i++ {
ch <- i // Envia valores para o channel
fmt.Println("Valor enviado:", i)
}
close(ch) // Fecha o channel após enviar todos os valores
}
func receberDados(ch <-chan int) {
for {
valor, ok := <-ch // Recebe valores do channel
if !ok {
fmt.Println("Channel fechado.")
return
}
fmt.Println("Valor recebido:", valor)
}
}
func main() {
// Criar um channel
dados := make(chan int)
// Executar a função para enviar dados em uma goroutine
go enviarDados(dados)
// Executar a função para receber dados em outra goroutine
go receberDados(dados)
// Aguardar um pouco para não terminar imediatamente
fmt.Println("Aguardando...")
fmt.Scanln()
}
Neste exemplo:
enviarDados
envia valores de 1 a 5 para o channel dados e fecha o channel após enviar todos os valores.receberDados
recebe os valores do channel dados e os imprime.- As duas goroutines (
enviarDados
ereceberDados
) executam simultaneamente, sincronizadas pelo channel.
Isso demonstra como os channels podem ser usados para transmitir dados de forma segura e sincronizada entre goroutines em Go.
- Define um canal somente para leitura.
- Apenas permite operações de recebimento (
valor := <-ch
), não é possível enviar valores para este canal. - Útil quando queremos garantir que nenhuma goroutine acidentalmente envie valores para o canal.
Exemplo:
ch := make(<-chan int) // Canal de leitura
- Define um canal somente para escrita.
- Apenas permite operações de envio (
ch <- valor
), não é possível receber valores deste canal. - Útil quando queremos garantir que nenhuma goroutine acidentalmente leia valores do canal.
Exemplo:
ch := make(chan<- int) // Canal de escrita
- Define um canal para leitura e escrita.
- Permite tanto operações de envio (
ch <- valor
) quanto de recebimento (valor := <-ch
). - É o tipo de canal mais comum e versátil.
Exemplo:
ch := make(chan int) // Canal de leitura e escrita
Estes tipos de channels são úteis para diferentes cenários. Canais de leitura (<-chan
) e escrita (chan<-
) são usados quando queremos restringir o uso do canal para apenas uma operação específica, garantindo mais segurança e clareza no código. Já canais de leitura e escrita (chan
) são os mais utilizados, pois oferecem a capacidade de envio e recebimento de valores, sendo essenciais para a comunicação entre goroutines em Go.
Para compilar um arquivo Go e gerar o arquivo binário executável, você pode usar o comando go build
seguido do nome do arquivo. Por exemplo, se o seu arquivo Go se chama main.go
, você pode usar o seguinte comando no terminal:
go build main.go
Este comando compilará o arquivo main.go
e criará um arquivo binário executável chamado main
(ou main.exe
no Windows) no mesmo diretório onde o arquivo Go está localizado.
Para executar o arquivo binário gerado após a compilação, você pode simplesmente digitar o nome do arquivo no terminal (ou prompt de comando no Windows) precedido de ./
no Linux e macOS, ou apenas o nome no Windows. Por exemplo:
No Linux e macOS:
./main
No Linux e macOS:
main.exe
Isso executará o arquivo binário main
, que é o programa que você compilou a partir do arquivo Go. Você verá a saída do programa no terminal. Certifique-se de estar no mesmo diretório em que o arquivo binário foi criado ou especificar o caminho completo para o arquivo se estiver em um diretório diferente.
Durante este estudo, exploramos os fundamentos da linguagem de programação Go (Golang), desde os conceitos básicos como variáveis, funções, e estruturas de controle, até tópicos mais avançados como concorrência, canais, e interfaces. Aqui estão alguns pontos-chave:
- Simplicidade e Eficiência: Go foi projetada para ser uma linguagem simples, clara e eficiente, com sintaxe concisa e poderosa.
- Concorrência e Goroutines: A concorrência em Go é impulsionada pelas goroutines, que são leves e podem ser executadas em paralelo. Isso permite que escrevamos programas concorrentes de forma simples e eficiente.
- Canais (Channels): Os canais são a principal forma de comunicação entre goroutines em Go. Eles permitem a sincronização e a troca de dados de forma segura e eficiente.
- Tipos de Channels: Existem canais de leitura (
<-chan
), escrita (chan<-
), e leitura e escrita (chan
) para atender às diferentes necessidades de comunicação entre goroutines. - Interfaces: As interfaces em Go permitem a abstração de comportamentos comuns, permitindo a implementação de tipos diferentes com métodos similares. Isso promove o polimorfismo e a reutilização de código.
- Estruturas e Métodos: Utilizamos structs para representar dados estruturados e métodos para associar comportamentos específicos a esses tipos de dados.
- Compilação e Execução: Para compilar um programa Go, usamos o comando
go build
. Para executar o programa gerado, basta chamar o arquivo binário resultante.
Em resumo, Go é uma linguagem poderosa, projetada para simplicidade, eficiência e concorrência. Com sua sintaxe limpa e recursos avançados, é uma excelente escolha para desenvolvimento de software moderno e escalável.