Sistemas de Tipos Avançados em Go
Em lições anteriores, você aprendeu que um sistema de tipo, nada mais é do que a forma como os dados são classificados, e como diferentes operações podem ser realizadas nesses dados.
No GoLang, o sistema de tipos é classificado em duas grandes categorias, estático e forte 🤓
No tipo estático, os tipos das variáveis são definidos em tempo de compilação, evitando erros comuns antes da execução do programa.
Já no tipo forte, não há conversão implícita entre tipos diferentes, exigindo que o desenvolvedor faça conversões explícitas quando necessário.
Anteriormente, você aprendeu a usar dois tipos no GoLang:
Só que além desses tipos, existem outros tipos um pouco mais avançados, que são:
struct
- inheritance (existe em Go?)
interface
any
Tipos estes que iremos conhecer agora 😉
Criando seu projeto de testes
Dentro da pasta JornadaGoLang, nós iremos criar uma nova pasta chamada de 11-sistemas-de-tipos-avancados-em-go
, onde dentro dela, vamos criar o nosso arquivo main.go
:
package main
func main(){
}
Feito isso, vamos começar pelo tipo struct
😁
Conhecendo o tipo struct
Se você já trabalhou com Javascript ou Typescript, é bem provável que já tenha ouvido falar de objetos, certo?
Um objetivo nada mais é do que uma espécie de map
do GoLang, onde você pode definir chaves e valores customizados.
Sendo assim, podemos dizer que uma struct
, nada mais é do que um tipo de dado composto que permite agrupar diferentes tipos de variáveis (chamados de campos).
E como dito, ele funciona de forma semelhante a objetos em linguagens orientadas a objetos, mas sem suporte a herança direta.
Resumindo, um struct
é uma estrutura que reúne múltiplos campos de diferentes tipos em uma única entidade, podendo ser utilizados para representar objetos do mundo real, como um usuário, um produto ou qualquer outra entidade.
A sintaxe de uma struct
é a seguinte:
type NomeDaStruct struct {
Campo1 Tipo1
Campo2 Tipo2
Campo3 Tipo3
}
Vejamos agora, um exemplo básico de sua utilização:
package main
import "fmt"
// Definindo uma struct chamada Pessoa
type Pessoa struct {
Nome string
Idade int
}
func main() {
// Criando uma instância da struct
p := Pessoa{
Nome: "João",
Idade: 30,
}
fmt.Println(p) // Saída: {João 30}
}
Observe no exemplo acima, que criamos uma variável por tipo de inferência chamada de p
, onde faz o uso da struct
chamada Pessoa
.
No caso do exemplo acima, pense em uma struct
como uma espécie de planta arquitetônica, onde definimos o formato e a estrutura de um objeto.

Onde cada campo da struct
representa um detalhe específico, assim como os cômodos em um projeto de construção, permitindo organizar e acessar informações de forma clara e eficiente.
É importante ressaltar que uma struct
faz parte de uma estrutura ou abstracional que é utilizada em muitas implementações de padrões de projeto (Design Pattern).
Além disso, você pode inicializar diversas variáveis seguindo uma mesma struct
, observe:
package main
import "fmt"
// Definindo uma struct chamada Pessoa
type Pessoa struct {
Nome string
Idade int
}
func main() {
// Criando uma instância da struct
p := Pessoa{
Nome: "João",
Idade: 30,
}
fmt.Println(p) // Saída: {João 30}
// Criando uma segunda instância da struct
p2 := Pessoa{
Nome: "Micilini Roll",
Idade: 27,
}
fmt.Println(p2) // Saída: {Micilini Roll 27}
}
Formas de se inicializar uma struct em go
No GoLang, temos algumas formas diferentes de se inicializar uma struct
, vejamos o uso de cada uma delas abaixo 😉
📌 A) Forma Nomeada (Recomendada para Legibilidade)
Para uma melhor legibilidade no seu código, a própria documentação do GoLang recomenda que uma struct
seja criada de forma nomeada (como vimos anteriormente):
p := Pessoa{
Nome: "Ana",
Idade: 25,
}
📌 B) Forma Posicional (Menos Legível)
Além disso, você pode criar uma nova struct
seguindo uma formatação mais simplista, porém seguir dessa forma torna seu código menos legível:
p := Pessoa{"Carlos", 40}
Observação: é importante ressaltar que a ordem dos campos importa e faz toda diferença na hora em que você estiver criando uma struct
.
📌 C) Inicialização Vazia
Além disso, e assim como em qualquer outro tipo no GoLang, é possível inicializar uma variável contendo uma struct
sem dados, observe:
var p Pessoa
fmt.Println(p) // Saída: { 0 }
Acessando e alterando campos de uma struct
Diferente do que acontece em um array
, slice
ou um map
, em que precisamos informar um índice, no caso de uma struct
, você pode acessar seus campos usando o operador ponto (.
), observe:
p := Pessoa{"João", 28}
// Acessando campos
fmt.Println(p.Nome) // Saída: João
// Modificando campos
p.Idade = 29
fmt.Println(p.Idade) // Saída: 29
Note que usamos o operador ponto em conjunto com o Println()
para mostrar um dado, e logo mais a baixo, usamos o mesmo operador, só que em conjunto com o operador de atribuição para o armazenamento de um novo dado.
Observação: não é possível remover um campo de uma struct
no tempo de execução da aplicação, ou seja, você não pode remover o campo Nome
ou Idade
do struct
pessoa só porque você quer. Portanto, lembre-se de criar uma struct
contendo somente os campos que você irá precisar.
Vejamos abaixo, um exemplo de uma struct em conjunto com um ponteiro:
p := &Pessoa{"Ana", 32}
// Modificando via ponteiro
p.Idade = 33
fmt.Println(p) // Saída: &{Ana 33}
fmt.Println(p.Nome) // Saída: Ana
fmt.Println(p.Idade) // Saída: 33
Observação: O Go gerencia automaticamente a desreferenciação, então p.Nome
funciona mesmo sendo um ponteiro.
Criando struct aninhadas (composição)
Da mesma forma como você viu na lição de tipos compostos (usando map), você também pode criar estruturas aninhadas usando struct
, observe:
type Endereco struct {
Rua string
Numero int
}
type Pessoa struct {
Nome string
Idade int
Endereco Endereco
}
func main() {
p := Pessoa{
Nome: "Lucas",
Idade: 27,
Endereco: Endereco{
Rua: "Rua das Flores",
Numero: 123,
},
}
fmt.Println(p.Nome) // Saída: Lucas
fmt.Println(p.Endereco.Rua) // Saída: Rua das Flores
}
No exemplo acima, note que criamos uma referência da struct Endereco
dentro da struct Pessoa
.
Isso torna a estrutura um pouco mais complexa e organizada.
Por que criar estruturas do tipo struct aninhadas?
Simples, quando criamos estruturas aninhadas, isso nos permite ter um código mais modular e bem organizado, em vez de ter apenas uma única estrutura gigantesca com todos os dados juntos e misturados.
Além disso, ter diversas estruturas separadas, pode te ajudar futuramente na reutilização dessas mesmas estruturas, pois permite reutilizar tipos (ou componentes) sem duplicação de código, já que uma struct
pode ser usada em várias outras estruturas, sem a necessidade de herdar seus campos.
Criando struct usando composição anônima (embedding)
No GoLang, você pode embutir uma struct
em outra, permitindo acesso direto aos campos da struct
embutida, observe:
type Animal struct {
Especie string
}
type Cachorro struct {
Animal // Embutindo a struct Animal
Raca string
}
func main() {
c := Cachorro{
Animal: Animal{Especie: "Canino"},
Raca: "Labrador",
}
fmt.Println(c.Raca) // Saída: Labrador
fmt.Println(c.Especie) // Acessa diretamente o campo da struct Animal
}
Note que dentro da variável c
, nos instanciamos uma struct
chamada Animal
dentro da struct Cachorro
.
Utilizando métodos em structs
Você sabia que é possível associar diversos métodos a uma struct? 🤔
Sim, no GoLang, você pode criar um método (função) e associá-la a um tipo específico, vejamos:
type Pessoa struct {
Nome string
}
// Método associado à struct Pessoa
func (p Pessoa) Saudacao() {
fmt.Println("Olá,", p.Nome)
}
func main() {
p := Pessoa{"Amanda"}
p.Saudacao() // Saída: Olá, Amanda
}
No exemplo acima, criamos uma variável chamada p
que contém a struct Pessoa
. Em seguida, associamos ao p
, a função Saudacao()
, que recebe uma struct
do tipo Pessoa
, e mostra uma mensagem de boas vindas no terminal.
Um outro exemplo, e até mais simples de se entender, é criando uma função que mostra os valores existentes em uma struct
, vejamos:
package main
import "fmt"
// Definindo a struct Endereco
type Endereco struct {
Rua string
Numero int
Cidade string
}
// Função que recebe um Endereco e exibe as informações no console
func mostrarEndereco(endereco Endereco) {
fmt.Println("Endereço completo:")
fmt.Println("Rua:", endereco.Rua)
fmt.Println("Número:", endereco.Numero)
fmt.Println("Cidade:", endereco.Cidade)
}
func main() {
// Criando uma instância de Endereco
meuEndereco := Endereco{
Rua: "Rua das Palmeiras",
Numero: 123,
Cidade: "São Paulo",
}
// Chamando a função passando o Endereco
mostrarEndereco(meuEndereco)
}
Comparando structs
Um ponto importante que você precisa entender é que struct
são comparáveis umas com as outras, isto é, se todos os seus campos também forem compatíveis, vejamos:
package main
import "fmt"
type Pessoa struct {
Nome string
Idade int
}
func main() {
p1 := Pessoa{"João", 25}
p2 := Pessoa{"João", 25}
p3 := Pessoa{"Micilini", 250}
fmt.Println(p1 == p2) // Saída: true
fmt.Println(p1 == p3) // Saída: false
}
Para fechar com chave de ouro esse assunto sobre structs
, gostaria que você desse uma lida nesses pontos chave:
struct
é um tipo composto que agrupa múltiplos campos.- Pode ser inicializada de várias formas (nomeada, posicional, vazia).
- Usa ponteiros para modificar valores diretamente.
- Permite embedding (substitui herança).
- Pode conter métodos associados.
- Suporta tags para serialização (ex.: JSON) como veremos em lições futuras.
Por fim, e não menos importante, aqui vai uma dica de mestre: sempre escolha seguir pela composição do que por uma herança 😄
A famosa fragilidade da herança ocorre quando uma pequena mudança na classe base pode afetar diversas subclasses, quebrando comportamentos em várias partes do sistema, e por isso a composição é mais vantajosa do que herança.
Lembrando que a composição pois oferece mais flexibilidade, melhor modularidade e evita os problemas comuns que a herança pode trazer, como o acoplamento excessivo e a fragilidade do código.
Portanto, como diz a frase, essa é uma "dica de mestre" que vale a pena seguir, especialmente ao trabalhar com GoLang! 😄
Criando tipos personalizados em Go
Com a linguagem GoLang, é possível criar tipos personalizados, ou seja, a partir de um tipo básico, você pode criar o seu próprio tipo!
Isso é feito por meio de um receiver de um método que você deseja colocar como tipo personalizado, observe:
package main
import "fmt"
type segredo string
func (s segredo) possoEntrar(termo string) bool {
if termo == "APTLEK" {
return true
}
return false
}
func main() {
var s segredo
fmt.Println(s.possoEntrar("APTLEK"))
}
O código define um tipo personalizado chamado segredo
, que é baseado em uma string
.
Em seguida, um método chamado possoEntrar
é associado a esse tipo, que verifica se uma palavra fornecida como argumento é igual a "APTLEK"
. Se for, o método retorna true
, caso contrário, retorna false
.
Structs VS Maps: Quando usar cada um deles?
Talvez você tenha percebido que as estruturas structs
e maps
são relativamente similares no quesito de funcionalidade, certo?
Ambas trabalham e recebem tipos básicos, e contém uma estrutura similar de chave e valor.
Apesar de serem extremamente parecidos a primeira vista, cada uma tem características e casos de uso específicos que fazem com que uma seja preferível à outra em determinadas situações.
Começando pelo quesito desempenho:
structs
: São mais rápidas e eficientes em termos de desempenho, especialmente quando você está lidando com dados fixos e bem definidos. Isso ocorre porque as structs
têm um layout de memória contíguo e acesso direto aos seus campos.
maps
: São mais lentos, pois têm uma sobrecarga maior para gerenciar as operações de hash, alocação dinâmica e resolução de colisões. A busca em um map
é mais lenta em comparação com o acesso direto aos campos de uma struct
.
Falando um pouco sobre a segurança e tipagem:
structs
: São fortemente tipadas, e isso significa que, quando você define uma struct
, você pode especificar tipos claros para cada campo, o que ajuda a evitar erros, já que o compilador garante que você está acessando os dados de forma consistente e segura.
maps
: Embora sejam flexíveis e você possa usar chaves e valores de diferentes tipos, essa flexibilidade pode levar a erros difíceis de detectar durante o desenvolvimento, como tentar acessar uma chave inexistente ou misturar tipos de dados de forma indevida.
Considerando a legibilidade (leitura) e manutenção do código:
structs
: São mais fáceis de ler e manter, especialmente quando você está lidando com entidades que têm um formato fixo e bem definido (por exemplo, uma struct que representa um usuário com nome, idade e email). O código é mais legível, e você pode acessar campos específicos de forma direta.
maps
: São úteis quando você precisa de um conjunto de dados mais flexível ou dinâmico, mas podem ser mais difíceis de ler e entender, já que o acesso aos dados não é tão explícito quanto com uma struct
.
Já com relação a validação e restrição de uso:
structs
: Permitem a definição de comportamentos e validações associadas a um objeto, por exemplo, você pode ter funções associadas à struct
que garantem que os dados são manipulados corretamente.
maps
: São mais simples e não permitem facilmente associar métodos ou validações diretamente aos dados.
Por fim, e não menos importante, temos diferenças gritantes correlacionadas a modelagem de dados:
structs
: São ideais para modelar dados que têm uma estrutura fixa, como registros de banco de dados, objetos de domínio ou dados de entrada/saída de APIs. Quando você sabe exatamente quais campos deseja armazenar e a estrutura é bem definida, as structs
são uma escolha natural.
maps
: São úteis quando a estrutura de dados não é fixa e você precisa de flexibilidade para adicionar ou remover campos dinamicamente. Isso pode ser útil em cenários como o armazenamento de pares chave-valor ou a manipulação de dados dinâmicos.
Agora vejamos algumas diferenças no uso de structs
e maps
:
type Person struct {
Name string
Age int
Email string
}
person := Person{Name: "John", Age: 30, Email: "john@example.com"}
fmt.Println(person.Name) // Acesso direto e seguro
person := map[string]interface{}{
"Name": "John",
"Age": 30,
"Email": "john@example.com",
}
fmt.Println(person["Name"]) // Acesso menos seguro, precisa de verificação de chave
Quando eu devo usar um map?
- Quando você não sabe as chaves de antemão e precisa de flexibilidade para armazenar pares chave-valor dinâmicos.
- Quando você precisa de um mecanismo rápido de busca e não se importa com a tipagem ou a ordem dos dados.
Quando eu devo usar um struct?
- Quando você precisa de uma estrutura de dados bem definida e com campos fixos:.
- Quando você precisa de uma representação de dados que se assemelhe a um objeto ou entidade do mundo real.
- Quando você precisa de encapsulamento e comportamento adicional.
- Quando você precisa garantir tipos fortes e um design robusto.
- Quando você deseja a ordem e estrutura dos dados em um formato mais rígido.
Portanto, podemos dizer que:
Use map quando precisar de flexibilidade e dinamismo, com busca eficiente, sem se importar com a ordem dos dados ou tipagem rígida.
Use struct quando precisar de uma estrutura bem definida com campos fixos, tipagem forte, e a capacidade de adicionar comportamentos ao conjunto de dados, sendo ideal para modelagem de entidades e objetos do mundo real.
Agora que você já entende quando e como fazer o uso de cada um deles dentro do seu projeto, vamos partir para o próximo tópico, o de interfaces
.
O que são interfaces?
No universo da linguagem de programação, uma interface
é um tipo de estrutura que define um conjunto de métodos ou comportamentos, sem a necessidade de especificar como esses métodos devem ser implementados.
Ela age como uma espécie de contrato, especificando quais métodos precisam ser implementados, mas sem fornecer a implementação concreta.
Observação: A interface
descreve o que um tipo pode fazer, mas não como ele faz.
Para exemplificar, imagine que você criou algumas funções em GoLang que fazem operações específicas relacionadas a dados de uma pessoa.
Onde tais operações podem ser simples, como mostrar o nome e a idade de uma determinada pessoa, vejamos um exemplo prático:
package main
import "fmt"
// Definindo a struct Pessoa
type Pessoa struct {
Nome string
Idade int
}
// Função para mostrar o nome da pessoa
func MostrarNome(p Pessoa) {
fmt.Println("Nome:", p.Nome)
}
// Função para mostrar a idade da pessoa
func MostrarIdade(p Pessoa) {
fmt.Println("Idade:", p.Idade)
}
func main() {
// Criando uma instância de Pessoa
pessoa := Pessoa{
Nome: "Carlos",
Idade: 30,
}
// Chamando as funções para mostrar o nome e a idade
MostrarNome(pessoa)
MostrarIdade(pessoa)
}
No código acima, criamos uma struct
que possui dois campos: Nome
e Idade
. Em seguida criamos duas funções responsáveis por mostrar os dados dessa struct
(MostrarNome
e MostrarIdade
).
Por fim, dentro de main()
, criamos uma pessoa e chamamos as funções responsáveis por exibir os respectivos nome e idade.
Se fossemos fazer isso usando o conceito de interfaces
, em vez de criarmos funções independentes, nós associaríamos esses métodos a essas interfaces
da seguinte forma:
package main
import "fmt"
// Definindo a interface Exibivel
type Exibivel interface {
MostrarNome()
MostrarIdade()
}
// Definindo a struct Pessoa
type Pessoa struct {
Nome string
Idade int
}
// Implementando o método MostrarNome para a struct Pessoa
func (p Pessoa) MostrarNome() {
fmt.Println("Nome:", p.Nome)
}
// Implementando o método MostrarIdade para a struct Pessoa
func (p Pessoa) MostrarIdade() {
fmt.Println("Idade:", p.Idade)
}
func main() {
// Criando uma instância de Pessoa
pessoa := Pessoa{
Nome: "Carlos",
Idade: 30,
}
// Criando uma variável do tipo Exibivel e atribuindo a pessoa
var e Exibivel = pessoa
// Chamando os métodos da interface
e.MostrarNome()
e.MostrarIdade()
}
Primeiro nós criamos uma interface
(contrato), que nós diz que ela deve implementar dois métodos, que são MostrarNome()
e MostrarIdade()
.
Em seguida, nos criamos esses métodos de maneira avulsa no código. Além disso, criamos uma struct
chamada Pessoa
, que implementa esses dois métodos, fazendo com que Pessoa
agora "cumpra" a interface Exibivel
.
Por fim, dentro do main()
, criamos uma variável e do tipo Exibivel
, que recebe uma instância de Pessoa
.
Como Pessoa
implementa a interface
, podemos chamar alguns métodos da interface
de maneira automática, tais como: e.MostrarNome()
e e.MostrarIdade()
.
A proósito, a sintaxe de uma interface
é a seguinte:
type NomeDaInterface interface {
Metodo1()
Metodo2(parametro Tipo) TipoRetorno
// outros métodos
}
O conceito de interfaces
em GoLang, vem da programação orientada a objetos (OO), porém a forma como é implementada aqui, funciona de maneira totalmente diferente das outras linguagens, pois como você já viu, a declaração de uma interface
acontece de forma implícita.
Observe como o conceito de interfaces é aplicada a uma programação orientada a objetos, como é o caso do Typescript:
// Definindo uma interface base
interface Animal {
name: string;
speak(): void;
}
// Definindo uma classe que implementa a interface Animal
class Dog implements Animal {
name: string;
constructor(name: string) {
this.name = name;
}
speak(): void {
console.log(`${this.name} late!`);
}
}
// Definindo uma classe que herda de outra classe
class Cat extends Dog {
constructor(name: string) {
super(name);
}
speak(): void {
console.log(`${this.name} mia!`);
}
}
// Criando instâncias
const dog = new Dog("Rex");
dog.speak(); // Saída: Rex late!
const cat = new Cat("Mia");
cat.speak(); // Saída: Mia mia!
Note que fica fica bastante visível o conceito de heranças e interfaces quando a própria linguagem faz o uso do comando extends
.
Voltando ao GoLang, para entendermos melhor, vamos imaginar que precisamos criar uma interface
que deve conter um único método chamado de ToString()
, que será responsável por receber structs
e retornar os campos dessa´s structs
em formato de texto (para ser exibido no terminal):
package main
import "fmt"
type imprimivel interface {
toString() string
}
type pessoa struct {
nome string
sobrenome string
}
type produto struct {
nome string
preco float64
}
// interfaces são implementadas implicitamente
func (p pessoa) toString() string {
return p.nome + " " + p.sobrenome
}
func (p produto) toString() string {
return fmt.Sprintf("%s - R$ %.2f", p.nome, p.preco)
}
func imprimir(x imprimivel) {
fmt.Println(x.toString())
}
func main() {
var s imprimivel = pessoa{"Micilini", "Roll"}
fmt.Println(s.toString())
imprimir(s)
p = produto{"Oculus Rift 3", 5800.89}
fmt.Println(p.toString())
imprimir(p)
p2 := produto{"RTX 59090 TI", 30000.99}
imprimir(p2)
}
Note que diferente de outras linguagens (caso você às conheça), a interface
imprimivel
não foi associada de forma explícita em uma struct
. (Como aconteceu no Typescript usando o comando extends).
Além disso, podemos usar a interface
em conjunto com diversas estruturas diferentes de struct
🙃
Composição de interfaces
Uma funcionalidade bastante comum entre os usos de interfaces
no GoLang, é um conceito conhecido como composição de interfaces.
É um recurso poderoso que permite que você construa novas interfaces
a partir de outras interfaces
.
Ou seja, uma interface
pode "incluir" outras interfaces, permitindo que você crie interfaces
mais complexas de forma modular.
Quando uma interface
é composta por outras, ela herda os métodos dessas interfaces
. Isso permite que você crie tipos mais específicos com a combinação de comportamentos definidos em diferentes interfaces
.
Vejamos um exemplo dessa composição:
package main
import "fmt"
// Definindo a interface "Nomeado" com o método "MostrarNome"
type Nomeado interface {
MostrarNome()
}
// Definindo a interface "Idade" com o método "MostrarIdade"
type Idade interface {
MostrarIdade()
}
// Definindo a interface composta "Exibivel" que inclui "Nomeado" e "Idade"
type Exibivel interface {
Nomeado // Incluindo a interface Nomeado
Idade // Incluindo a interface Idade
}
// Definindo a struct Pessoa
type Pessoa struct {
Nome string
Idade int
}
// Implementando o método MostrarNome para a struct Pessoa
func (p Pessoa) MostrarNome() {
fmt.Println("Nome:", p.Nome)
}
// Implementando o método MostrarIdade para a struct Pessoa
func (p Pessoa) MostrarIdade() {
fmt.Println("Idade:", p.Idade)
}
func main() {
// Criando uma instância de Pessoa
pessoa := Pessoa{
Nome: "Carlos",
Idade: 30,
}
// Criando uma variável do tipo Exibivel, que é composta por Nomeado e Idade
var e Exibivel = pessoa
// Chamando os métodos da interface Exibivel
e.MostrarNome()
e.MostrarIdade()
}
No exemplo acima, criamos a seguinte estrutura:
- Interface Nomeado: Define o método
MostrarNome()
. - Interface Idade: Define o método
MostrarIdade()
. - Interface Exibivel: Composta pelas interfaces
Nomeado
eIdade
, o que significa que qualquer tipo que implemente a interfaceExibivel
deve implementar todos os métodos de Nomeado e Idade. - Struct Pessoa: Implementa os métodos
MostrarNome()
eMostrarIdade()
da interfaceExibivel
.
Por fim, dentro do main()
, nós criamos uma variável chamada e
do tipo Exibivel
, que pode armazenar qualquer tipo que implemente tanto a interface
Nomeado
quanto a interface Idade
. No caso, Pessoa
implementa ambas as interfaces, então pode ser atribuída à variável e
.
Trabalhando com o tipo Any
O any
em Go é um tipo especial que foi introduzido no Go 1.18 como parte das melhorias no sistema de tipos genéricos da linguagem.
O any
é um alias para o tipo interface{}
, que, por sua vez, é o tipo vazio em Go.
Ele pode ser usado para representar qualquer tipo de valor, ou seja, um valor de qualquer tipo pode ser atribuído a uma variável do tipo any
.
E isso pode ser feito da seguinte forma:
package main
import "fmt"
// Função que aceita um valor de qualquer tipo
func imprimirValor(v any) {
fmt.Println(v)
}
func main() {
// Chamando a função com diferentes tipos
imprimirValor(42) // Um inteiro
imprimirValor("Olá, Go!") // Uma string
imprimirValor(3.14) // Um float
imprimirValor(true) // Um booleano
}
Note que criamos uma variável dentro da função imprimirValor
que é do tipo any
, e que pode receber qualquer tipo de valor.
Usar o any
torna o código mais intuitivo e fácil de se entender, sem perder a flexibilidade de trabalhar com diferentes tipos de dados.
Por outro lado, seu código pode ficar mais suscetível a erros, uma vez que any
pode aceitar qualquer valor, e uma hora ou outra, você pode acabar considerando valores fixos.
Além disso, quando criamos uma variável (não temporária) do tipo any
e atribuímos um determinado a ela, aquela variável poderá ter seu tipo alterado futuramente, exemplo:
package main
import "fmt"
func main() {
var value any // Declarando uma variável do tipo any
value = 42 // Atribuindo um valor inteiro
fmt.Println(value)
value = "Hello, Go!" // Atribuindo um valor string
fmt.Println(value)
// Tentando atribuir um valor de tipo diferente
// Não podemos, por exemplo, atribuir uma função diretamente sem uma conversão explícita
// value = func() {} // Isso causaria erro de compilação
// Para alterar o tipo de uma variável de any para outro tipo, precisamos usar conversão de tipo
if str, ok := value.(string); ok {
fmt.Println("O valor é uma string:", str)
} else {
fmt.Println("O valor não é uma string.")
}
}
Note que criamos uma variável chamada value
do tipo any
, que primeiro recebeu um int
(42) e no final se tornou uma string
("Hello, Go!").
Conhecendo o tipo genérico em Go
Os tipos genéricos em Go foram introduzidos no Go 1.18, permitindo que você escreva funções, tipos, e estruturas de dados que podem operar com qualquer tipo sem perder a segurança do sistema de tipos estático da linguagem.
Os tipos genéricos, atuam de forma similar ao any
, e permitem que você escreva código que pode operar sobre diferentes tipos de dados sem a necessidade de duplicar código para cada tipo.
Vejamos um pequeno exemplo do tipo genérico em uso:
package main
import "fmt"
// Função genérica que aceita um parâmetro de tipo T
func imprimir[T any](valor T) {
fmt.Println(valor)
}
func main() {
imprimir(42) // Tipo int
imprimir("Olá, Go!") // Tipo string
imprimir(3.14) // Tipo float
imprimir(true) // Tipo bool
}
A função imprimir[T any](valor T)
é genérica. O tipo T
é o parâmetro de tipo que pode ser qualquer tipo, ou seja, você poderia colocar qualquer letra ou termo, mas por convenção, usamos T
.
O tipo any
especifica que o parâmetro T
pode ser de qualquer tipo.
Quando você chama imprimir(42)
, o tipo T
é resolvido como int
, e assim por diante.
Ao definir uma função ou tipo genérico, você pode especificar um ou mais parâmetros de tipo. No exemplo acima, usamos T any
, mas você pode usar quantos parâmetros de tipo precisar, exemplo:
package main
import "fmt"
// Função genérica com múltiplos parâmetros de tipo
func somar[T int | float64](a, b T) T {
return a + b
}
func main() {
fmt.Println(somar(10, 20)) // Soma com int
fmt.Println(somar(10.5, 20.5)) // Soma com float64
}
O tipo T
é limitado a int
ou float64
usando a sintaxe T int | float64
.
Isso significa que a função somar pode operar tanto com inteiros (int
) quanto com floats
.
Vejamos um outro exemplo do termo |
usando interfaces
:
package main
import "fmt"
// Interface que define um comportamento de tipo
type Adicionar interface {
int | float64
}
// Função genérica que aceita apenas tipos que sejam int ou float64
func somar[T Adicionar](a, b T) T {
return a + b
}
func main() {
fmt.Println(somar(10, 20)) // Soma com int
fmt.Println(somar(10.5, 20.5)) // Soma com float64
}
Quando usar tipos genéricos?
- Código reutilizável: Quando você precisa de funções ou tipos que operam com múltiplos tipos de dados.
- Abstração de dados: Quando você deseja abstrair o comportamento de dados sem perder a segurança do tipo.
- API flexível: Ao escrever bibliotecas ou pacotes que precisam ser utilizados com diferentes tipos de dados sem duplicar código.
Tipo Interface (Vazia)
Em Go, quando você usa uma interface vazia (interface{}
), isso significa que a variável pode armazenar qualquer tipo.
Isso ocorre porque interface{}
é o tipo que representa "sem especificação de tipo", ou seja, ela pode conter valores de qualquer tipo.
A partir do Go 1.18, você pode usar o any
em vez de interface{}
para melhorar a legibilidade do código, mas funcionalmente, ambos são equivalentes.
Entretanto, é interessante que você aprenda a usar o tipo interface{}
para armazenar e manipular valores de diferentes tipos:
package main
import "fmt"
func main() {
var coisa interface{}
// Atribuindo um inteiro
coisa = 3
fmt.Println(coisa) // Output: 3
// Atribuindo uma string
coisa = "olá, Go!"
fmt.Println(coisa) // Output: olá, Go!
// Atribuindo um float
coisa = 3.14
fmt.Println(coisa) // Output: 3.14
// Atribuindo um booleano
coisa = true
fmt.Println(coisa) // Output: true
}
Observação: coisa
é uma variável do tipo interface{}
, o que significa que pode armazenar qualquer valor, independentemente do tipo.
Note também que o tipo interface{}
é similar ao any
.
Existe herança em Go?
Não, como dito desde o início desta jornada, o GoLang não segue as regras do estilo tradicional da orientação a objetos, e por conta disso, o conceito de heranças não pode ser aplicado a ele, pelo menos não da maneira convencional.
Entretanto, como você viu anteriormente, o Go oferece um conceito de composição para alcançar a reutilização de código e comportamento semelhante à herança.
Vejamos um exemplo:
package main
import "fmt"
// Definindo uma estrutura base (pode ser considerada como uma "superclasse")
type Animal struct {
Name string
}
func (a *Animal) Speak() {
fmt.Println(a.Name, "fazendo som")
}
// Estrutura que "herda" da estrutura Animal
type Dog struct {
Animal // Composição
}
func (d *Dog) Speak() {
fmt.Println(d.Name, "late")
}
func main() {
dog := Dog{Animal{"Rex"}}
dog.Speak() // Saída: Rex late
}
Neste exemplo, Dog
"herda" os métodos de Animal
através da composição, mas a implementação do método Speak
é substituída (ou sobrecarregada) na estrutura Dog
.
Dessa forma, a linguagem promove a composição como a principal forma de reutilização de código, ao invés da herança clássica.
Repositório da lição
Todos os arquivos relacionados com esta lição, podem ser encontrados nos seguintes repositórios abaixo:
Conclusão
Nesta lição, você aprendeu a usar os tipos struct
, interface
e any
.
Até a próxima 😄