Programação Orientada a Objetos com Go: Conceitos Fundamentais - Classes, Objetos, Encapsulamento

Quando falamos em Programação Orientada a Objetos (POO), muitas pessoas pensam automaticamente em linguagens como Java, C# ou Python. Mas e o Go? Ele é orientado a objetos? A resposta é: sim, mas com um estilo próprio conforme falei no artigo passado.
Neste artigo, vamos explorar como os conceitos fundamentais da POO (como classes, objetos e encapsulamento) se manifestam na linguagem Go. Tudo com exemplos práticos, simples e diretos ao ponto.
Go tem classes?
Olha, sendo bem direto posso dizer que Go não possui o conceito de "classe" da forma tradicional como vemos em outras linguagens. Em vez disso, usamos structs para definir tipos compostos e métodos associados a esses tipos para dar comportamento. Vamos ver um exemplo do que seria uma "classe" Pessoa em Go.
type Pessoa struct {
Nome string
Idade int
}
Essa Pessoa é, de certa forma, a nossa "classe". Ela define uma estrutura de dados que podemos instanciar e manipular. Agora vou fazer um comparativo entre a classe em Java, que por muitas vezes é uma linguagem que se usa para ensinar POO e Go.
+--------------------------+ +---------------------------+
| Java (classe) | | Go (struct) |
+--------------------------+ +---------------------------+
| class Pessoa { | | type Pessoa struct { |
| String nome; | ---> | Nome string |
| int idade; | | Idade int |
| | | } |
| String apresentar() { | | func (p Pessoa) |
| return ... | | Apresentar() string { ... |
| } | | } |
| } | | |
+--------------------------+ +---------------------------+
Podemos ver que tanto os atributos quanto os métodos em Java ficam dentro da classe, já em Go por não termos classes, apenas os atributos ficam dentro da struct. Já os métodos declaramos "fora". O próximo passo é dar comportamento a ela através dos métodos. Vamos?
Métodos: como adicionar comportamento a structs.
Podemos associar métodos a structs usando receivers. Mas o que é um receiver você pode estar se perguntando agora. Bem, em Go, um receiver é um parâmetro especial associado a um método, que define o tipo de objeto sobre o qual o método opera. É semelhante ao conceito de this em outras linguagens orientadas a objetos, mas é mais flexível em Go, podendo ser um valor ou um ponteiro para um valor. Vejamos um exemplo:
func (p Pessoa) Apresentar() string {
return fmt.Sprintf("Olá, meu nome é %s e tenho %d anos.", p.Nome, p.Idade)
}
Esse método Apresentar funciona de forma parecida com um método de instância em linguagens como Java ou Python. A diferença é que aqui somos explícitos: dizemos exatamente a qual tipo ele pertence, e se o receiver é por valor ou por ponteiro.
E qual a diverença se o receiver é valor ou ponteiro? Bem, nesse caso usamos o receiver de valor quando queremos apenas ler um dado, sem alterar qualquer valor dentro da struct, como por exemplo ler o nome e idade no método que criamos. Já para o ponteiro é quando queremos atualizar algum valor internamente, fazer uma operação, por exemplo se incrementarmos a idade da pessoa.
Objetos: criando instâncias em Go.
Em Go, criamos instâncias de structs de forma bem simples:
p := Pessoa{
Nome: "Augusto",
Idade: 30,
}
Essa variável p é um "objeto" no sentido da POO — ele tem estado (dados como nome e idade) e comportamento (métodos como Apresentar).
Encapsulamento em Go
Bem, encapsulamento em Go também é diferente de outras linguagens, Go não usa palavras-chave como private ou public. Em vez disso, o encapsulamento é controlado pela capitalização dos nomes. Mas como assim? Bem, resumindo fica assim:
Letra maiúscula → Exportado (público)
Letra minúscula → Não exportado (privado ao pacote)
Isso nos leva a um estilo explícito e claro para encapsular dados que Go proporciona. Veja o exemplo da "classe" Pessoa, onde nome agora será privado e seu diagrama resumindo esse comportamento:
type Pessoa struct {
nome string // não exportado
Idade int // exportado
}
+-----------------------------+
| type Pessoa struct |
+-----------------------------+
| nome string ← privado|
| Idade int ← público|
+-----------------------------+
Além disso, criamos funções ou métodos para controlar o acesso aos campos, sendo apenas para acessar o valor ou seja este para atualizar um valor. Lembrando que a acessebilidade do método também será determinada pela capitalização da primeira letra eim?
// método público
func (p Pessoa) IdateAtual() int {
return p.Idade
}
// método privado
func (p *Pessoa) fazAniversario() {
p.Idade++
}
Esse controle simples, mas eficaz, estimula o uso consciente dos limites entre pacotes, favorecendo o design limpo e modular.
Boas práticas de encapsulamento em Go
- Campos privados, métodos públicos
- Expõe apenas o necessário. Campos devem ser manipulados através de métodos.
- Use construtores (NewPessoa)
- Garantem inicialização válida e mantêm invariantes
- Receivers por valor ou ponteiro, conforme o caso
- Mutação exige ponteiro, leitura simples pode ser por valor.
- Design orientado a pacotes
- Pacotes devem representar módulos com responsabilidade única. Cada pacote é uma "caixa preta" com API clara e bem definida.
E agora?
Embora Go não siga o modelo clássico da POO com classes e herança, ele permite aplicar os conceitos essenciais de forma simples, idiomática e poderosa. O encapsulamento, em especial, é incentivado por uma abordagem explícita de visibilidade baseada em pacotes.
No próximo artigo, vamos abordar como Go lida com herança e polimorfismo através de composição e interfaces, mostrando como esses conceitos se adaptam ao estilo da linguagem sem perder a expressividade e o poder da orientação a objetos.