Diferenças entre structs e classes

No Go, uma struct é um tipo de dado fundamental que agrupa “variáveis” (atributos) sob um único nome, similar a como uma classe agrupa propriedades e métodos em linguagens orientadas a objetos como Java ou C#. No entanto, existem diferenças significativas tanto na sintaxe quanto nos conceitos e funcionalidades oferecidos por structs em Go quando comparada com classes em outras linguagens.

Estrutura e Sintaxe

Struct

Em Go, uma struct é definida usando a palavra-chave struct. A definição de uma struct envolve apenas a declaração de atributos.

type Pessoa struct {
	Nome  string
	Idade int
}

Classe em Java

Uma classe normalmente não é composta somente por propriedades, mas também métodos, visibilidade (public, private, etc.), além de envolver conceitos como herança e polimorfismo.

public class Pessoa {
	
	private String nome;
	private int idade;
	
	public Pessoa(String nome, int idade) {
    this.nome = nome;
    this.idade = idade;
	}

	public String getNome() {
    return nome;
	}

	public void setNome(String nome) {
    this.nome = nome;
	}

	public int getIdade() {
    return idade;
	}

	public void setIdade(int idade) {
    this.idade = idade;
	}
}

Visibilidade

No Go, a visibilidade é controlada pela capitalização do nome dos atributos ou funções. Se uma propriedade ou método começa com uma letra maiúscula, ele é exportado (public) e pode ser acessado de outros pacotes. Se começa com uma letra minúscula, ele é não-exportado (private) e só pode ser acessado dentro do mesmo pacote.

type Pessoa struct {
	Nome  string  // Público (exportado)
	idade int     // Privado (não exportado)
}

Já em linguagem orientadas a objetos, além de existir uma keyword para definir as visibilidades, o mais comum é haver 3 níveis de visibilidade (public, private e protected).

Métodos

No Go, métodos podem ser definidos para qualquer tipo, inclusive para structs. No entanto, Go não agrupa métodos dentro da definição da struct, como se faz em linguagens orientadas a objetos. Em vez disso, métodos são definidos separadamente e associados a um tipo.

func (p *Pessoa) Saudacao() string {
	return "Olá, meu nome é " + p.Nome
}

Na maioria das linguagens orientadas a objetos (p. ex., Java), os métodos são definidos dentro da classe.

public class Pessoa {
	// atributos e métodos
	public String saudacao() {
		return "Olá, meu nome é " + this.nome;
	}
}

Herança

Go não suporta herança tradicional como em linguagens orientadas a objetos. Em vez disso, Go utiliza a composição para reutilização de código. Uma struct pode incluir outra struct, o que permite o acesso aos atributos e métodos da struct “inclusa”.

type Endereco struct {
	Rua   string
	Numero int
}

type Pessoa struct {
	Nome     string
	Idade    int
	Endereco // Composição
}

Em linguagens orientadas a objetos, a herança é uma característica central.

public class Pessoa extends Mamifero {
// atributos e métodos
}

Interfaces

Go usa interfaces para definir comportamentos que tipos (incluindo structs) podem implementar. As interfaces em Go são satisfeitas implicitamente, ou seja, um tipo implementa uma interface simplesmente ao ter os métodos da interface.

type Saudador interface {
	Saudacao() string
}

func (p Pessoa) Saudacao() string {
	return "Olá, meu nome é " + p.Nome
}

Em linguagens orientadas a objetos, as interfaces (ou tipos similares, como “abstract classes” em Java) são declaradas explicitamente e a classe deve mencionar que implementa a interface.

public interface Saudador {
	String saudacao();
}

public class Pessoa implements Saudador {
	public String saudacao() {
		return "Olá, meu nome é " + this.nome;
	}
}

Runtime

Além das diferenças já mencionadas com relação a sintaxe, existem diferenças significativas em como o runtime do Go lida com structs em comparação com como, por exemplo, a JVM (Java Virtual Machine) lida com classes.

Gerenciamento de Memória

Golang:

A memória é gerenciada pelo garbage collector (GC), de uma maneira otimizada para se ter maior performance e menor latência. Go usa uma estratégia de alocação de memória que pode ser dividida em duas partes principais: stack allocation e heap allocation.

  • Stack Allocation: Variáveis locais, incluindo structs, que são criadas dentro de funções, são frequentemente alocadas no stack, se o compilador puder determinar que elas não serão utilizadas fora dessa função. Isso torna a alocação e desalocação de memória muito rápida.
  • Heap Allocation: Se o compilador não puder provar que uma variável não será utilizada fora do escopo da função (por exemplo, se a variável é retornada ou usada por uma goroutine), então essa variável é alocada no heap.
func createPessoa() *Pessoa {
	p := Pessoa{Nome: "João", Idade: 30}
	return &p // Pode ser gerenciada pelo heap
}

Java (JVM):

Na JVM, todas as instâncias de objetos são alocadas no heap. Variáveis locais que são primitivas (e.g., int, float) são armazenadas no stack, mas instâncias de classes são sempre heap-allocated.

  • Heap Allocation: O heap é gerenciado pelo garbage collector. A JVM possui avançadas técnicas de coleta de lixo (como G1, ZGC) que tentam balancear entre throughput e latência.
public Pessoa createPessoa() {
	Pessoa p = new Pessoa("João", 30); // Sempre será alocado no heap
	return p;
}

Desempenho e Otimização

Golang:

  • Acesso por Valor: Por padrão, structs em Go são passadas por valor, o que pode ter implicações de desempenho dependendo do tamanho da struct. Porém, passar referências (ponteiros) também é comum para evitar copiar grandes structs.
  • Inline Function Calls: O compilador Go é projetado para ser eficiente e pode realizar otimizações como inlining de funções, para melhorar o desempenho.
  • Concurrency Model: Go é conhecido por seu modelo de concorrência baseado em goroutines e canais, que são leves em comparação com threads na JVM. Goroutines têm menor sobrecarga de memória e podem ser gerenciadas de forma muito eficiente pelo runtime.
func process(p Pessoa) {
// Struct é passada por valor
}

func processPointer(p *Pessoa) {
// Struct é passada por referência (ponteiro)
}

Java (JVM):

  • Acesso por Referência: Em Java, todas as variáveis de objeto são referências, o que significa que passar uma instância de um objeto para um método implica em passar uma referência para esse objeto. Isso evita a cópia de dados, mas implica em acesso indireto à memória, o que pode introduzir alguma sobrecarga.
  • Just-In-Time Compilation (JIT): A JVM usa JIT compilation para traduzir bytecode em código nativo em tempo de execução, otimizando dinamicamente o desempenho das aplicações. A JVM pode realizar várias otimizações agressivas que não são possíveis em compilação estática.
  • Thread Model: Java usa threads nativas do sistema operacional, o que pode ter mais sobrecarga em comparação com goroutines em Go. No entanto, a JVM tem otimizações avançadas para gerenciar threads de maneira eficiente.
public void process(Pessoa p) {
// Objeto é passado por referência
}

Modelo de Concorrência

Golang:

  • Goroutines: São “threads leves” gerenciadas pelo runtime do Go. São muito mais leves que threads do SO e a criação de uma goroutine é muito rápida e de baixo custo.
  • Channels: Facilita a comunicação segura entre goroutines sem a necessidade de locks explícitos.

Java (JVM):

  • Threads: Java usa threads do SO, que são mais pesadas e mais caras para criar e gerenciar comparado com goroutines.
  • Executors e Futures: Java fornece abstrações de alto nível (Executors, Futures, CompletableFutures) para trabalhar com concorrência, mas geralmente com mais complexidade e sobrecarga em comparação com os modelos de concorrência de Go.

Conclusão

Structs em Go e classes em linguagens orientadas a objetos podem parecer semelhantes na superfície, porém, suas funcionalidades, conceitos e práticas divergem significativamente. Go favorece a composição sobre a herança, oferece um modelo simples de visibilidade de membros e utiliza interfaces com implementação implícita, contrastando com a abordagem mais complexa e rica em funcionalidades das classes em linguagens OOP tradicionais.

No quesito execução, o runtime do Go e a JVM lidam de forma bastante diferente com suas respectivas construções (structs e classes). O Go é otimizado para alocação de memória eficiente e concorrência leve utilizando goroutines, enquanto a JVM é conhecida por suas otimizações agressivas em tempo de execução e modelo de thread mais pesado.

Em resumo, essas diferenças refletem as filosofias dos projetos implementados em cada linguagem, o que acabará impactando como os desenvolvedores devem abordar o desempenho e a escalabilidade em suas aplicações.

Até a próxima!


Faça parte da comunidade!

Receba os melhores conteúdos sobre Go, Kubernetes, arquitetura de software, Cloud e esteja sempre atualizado com as tendências e práticas do mercado.

Livros Recomendados

Abaixo listei alguns dos melhores livros que já li sobre GO.

Um comentário sobre “Diferenças entre structs e classes

Deixe uma resposta