Tipos Avançados em Go

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 e Idade, o que significa que qualquer tipo que implemente a interface Exibivel deve implementar todos os métodos de Nomeado e Idade.
  • Struct Pessoa: Implementa os métodos MostrarNome() e MostrarIdade() da interface Exibivel.

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 😄

Criadores de Conteúdo

Foto do William Lima
William Lima
Fundador da Micilini

Inventor nato, escreve conteudos de programação para o portal da micilini.

Torne-se um MIC 🤖

Mais de 100 mic's já estão conectados na plataforma.