Go Admin

Como Usar Structs em Go: Definição, Embedding e Métodos em Produção Já leu

Structs em Go: Definição, Embedding e Métodos O que é uma Struct e Por Que Usar Uma struct em Go é um tipo de dado composto que agrupa múltiplos campos de tipos diferentes em uma única entidade. Diferentemente de linguagens orientadas a objetos clássicas, Go não possui classes, mas structs preenchem esse papel de forma elegante e eficiente. Você utiliza structs quando precisa representar conceitos do mundo real — um usuário, um produto, uma transação bancária — donde cada um desses conceitos possui características (campos) que devem ser agrupadas logicamente. A filosofia do Go é simplicidade. Structs refletem isso: não há herança complexa, não há construtores obrigatórios, não há getters e setters automáticos. Você define exatamente o que precisa, nada mais. Isso torna o código mais previsível e fácil de manter em projetos grandes. Definição e Inicialização de Structs Declarando uma Struct A sintaxe básica é direta. Você usa a palavra-chave seguida do nome e , listando os campos com

Structs em Go: Definição, Embedding e Métodos

O que é uma Struct e Por Que Usar

Uma struct em Go é um tipo de dado composto que agrupa múltiplos campos de tipos diferentes em uma única entidade. Diferentemente de linguagens orientadas a objetos clássicas, Go não possui classes, mas structs preenchem esse papel de forma elegante e eficiente. Você utiliza structs quando precisa representar conceitos do mundo real — um usuário, um produto, uma transação bancária — donde cada um desses conceitos possui características (campos) que devem ser agrupadas logicamente.

A filosofia do Go é simplicidade. Structs refletem isso: não há herança complexa, não há construtores obrigatórios, não há getters e setters automáticos. Você define exatamente o que precisa, nada mais. Isso torna o código mais previsível e fácil de manter em projetos grandes.

Definição e Inicialização de Structs

Declarando uma Struct

A sintaxe básica é direta. Você usa a palavra-chave type seguida do nome e struct, listando os campos com seus tipos:

package main

import "fmt"

type Pessoa struct {
    Nome  string
    Idade int
    Email string
}

func main() {
    // Inicialização usando campos nomeados (recomendado)
    p1 := Pessoa{
        Nome:  "Alice",
        Idade: 30,
        Email: "alice@example.com",
    }
    fmt.Println(p1)

    // Inicialização posicional (menos segura)
    p2 := Pessoa{"Bob", 25, "bob@example.com"}
    fmt.Println(p2)

    // Inicialização parcial (campos não mencionados recebem valor zero)
    p3 := Pessoa{Nome: "Charlie"}
    fmt.Println(p3) // {Charlie 0 }
}

Note que campos de struct em Go começam com letra maiúscula ou minúscula, determinando sua visibilidade. Maiúscula significa exportado (acessível fora do pacote), minúscula significa privado. No exemplo acima, Pessoa é exportada, assim como seus campos.

Tipos Aninhados e Campos com Tags

Structs também podem conter outras structs como campos. Além disso, você pode adicionar tags aos campos — metadados usados por bibliotecas externas como encoding/json:

package main

import (
    "encoding/json"
    "fmt"
)

type Endereco struct {
    Rua    string `json:"rua"`
    Cidade string `json:"cidade"`
    CEP    string `json:"cep"`
}

type Usuario struct {
    ID       int       `json:"id"`
    Nome     string    `json:"nome"`
    Endereco Endereco  `json:"endereco"`
}

func main() {
    usuario := Usuario{
        ID:   1,
        Nome: "Diana",
        Endereco: Endereco{
            Rua:    "Rua das Flores",
            Cidade: "São Paulo",
            CEP:    "01310-100",
        },
    }

    // Convertendo para JSON
    jsonData, _ := json.MarshalIndent(usuario, "", "  ")
    fmt.Println(string(jsonData))
}

As tags json:"campo" indicam qual chave JSON corresponde a cada campo da struct. Isso é especialmente útil ao trabalhar com APIs REST.

Embedding: Composição sobre Herança

O Conceito de Embedding

Go não suporta herança clássica. Em seu lugar, oferece embedding — uma forma elegante de composição onde você incorpora uma struct dentro de outra. O struct incorporado "promove" seus campos e métodos para a struct que o contém, criando uma relação de "é um" de forma composicional.

Imagine que você tem uma struct Animal com comportamentos comuns. Ao invés de criar Cachorro extends Animal, você incorpora Animal dentro de Cachorro. Assim, qualquer campo ou método de Animal é acessível diretamente através de Cachorro, sem necessidade de prefixo.

Exemplo Prático de Embedding

package main

import "fmt"

type Animal struct {
    Nome string
    Idade int
}

func (a Animal) Fazer_Som() string {
    return "Som genérico"
}

// Cachorro incorpora Animal
type Cachorro struct {
    Animal  // Embedding: sem nome de campo
    Raca    string
}

// Gato também incorpora Animal
type Gato struct {
    Animal
    Cor string
}

// Métodos específicos que sobrescrevem o método do Animal
func (c Cachorro) Fazer_Som() string {
    return "Au au!"
}

func (g Gato) Fazer_Som() string {
    return "Miau!"
}

func main() {
    cachorro := Cachorro{
        Animal: Animal{Nome: "Rex", Idade: 5},
        Raca:   "Labrador",
    }

    gato := Gato{
        Animal: Animal{Nome: "Whiskers", Idade: 3},
        Cor:    "Laranja",
    }

    // Acessando campos promovidos
    fmt.Println(cachorro.Nome)  // Rex
    fmt.Println(gato.Idade)     // 3

    // Métodos sobrescritos (polimorfismo)
    fmt.Println(cachorro.Fazer_Som()) // Au au!
    fmt.Println(gato.Fazer_Som())     // Miau!

    // Acessando método original via struct incorporado
    fmt.Println(Animal{Nome: "Desconhecido"}.Fazer_Som()) // Som genérico
}

Neste exemplo, Cachorro e Gato herdam os campos Nome e Idade de Animal através do embedding. Você pode acessá-los diretamente sem escrever cachorro.Animal.Nome. Além disso, cada tipo pode implementar sua própria versão do método Fazer_Som(), conseguindo polimorfismo sem herança.

Embedding Múltiplo

Go permite que uma struct incorpore mais de uma outra struct. Se houver conflito de nomes, você precisará ser explícito:

package main

import "fmt"

type Terrestre struct {
    Velocidade int
}

type Aquatico struct {
    Profundidade int
}

type Pato struct {
    Terrestre
    Aquatico
    Nome string
}

func main() {
    pato := Pato{
        Terrestre:  Terrestre{Velocidade: 40},
        Aquatico:   Aquatico{Profundidade: 2},
        Nome:       "Donald",
    }

    fmt.Println(pato.Velocidade)    // 40
    fmt.Println(pato.Profundidade)  // 2
    fmt.Println(pato.Nome)          // Donald
}

Métodos em Structs

Receptores e a Sintaxe de Métodos

Diferentemente de outras linguagens, Go não vincula métodos a structs através de definição de classe. Em seu lugar, você declara funções com um receptor — um parâmetro especial que vem antes do nome da função. O receptor estabelece a ligação entre a função e o tipo.

package main

import "fmt"
import "math"

type Circulo struct {
    Raio float64
}

// Método com receptor por valor
func (c Circulo) Area() float64 {
    return math.Pi * c.Raio * c.Raio
}

// Método com receptor por ponteiro
func (c *Circulo) Crescer(percentual float64) {
    c.Raio = c.Raio * (1 + percentual/100)
}

func main() {
    circulo := Circulo{Raio: 5}
    fmt.Printf("Área inicial: %.2f\n", circulo.Area()) // 78.54

    circulo.Crescer(50) // Cresce 50%
    fmt.Printf("Área após crescimento: %.2f\n", circulo.Area()) // 176.71
}

Receptor por valor vs. receptor por ponteiro: quando você usa (c Circulo), o método recebe uma cópia da struct. Modificações dentro do método não afetam o original. Quando usa (c *Circulo), o método recebe um ponteiro e pode modificar a struct original. Use receptor por valor para métodos que apenas leem dados, e receptor por ponteiro para métodos que modificam estado.

Interfaces e Métodos

Structs em Go implementam interfaces implicitamente. Uma interface define um conjunto de métodos. Qualquer struct que implemente todos esses métodos satisfaz automaticamente a interface, sem necessidade de declaração explícita:

package main

import "fmt"

type Veiculo interface {
    Velocidade_maxima() float64
    Descricao() string
}

type Carro struct {
    Marca string
    Modelo string
}

type Bicicleta struct {
    Tipo string
}

func (c Carro) Velocidade_maxima() float64 {
    return 250.0
}

func (c Carro) Descricao() string {
    return fmt.Sprintf("%s %s", c.Marca, c.Modelo)
}

func (b Bicicleta) Velocidade_maxima() float64 {
    return 40.0
}

func (b Bicicleta) Descricao() string {
    return fmt.Sprintf("Bicicleta %s", b.Tipo)
}

func ExibirVeiculo(v Veiculo) {
    fmt.Printf("%s com velocidade máxima de %.0f km/h\n", 
        v.Descricao(), 
        v.Velocidade_maxima())
}

func main() {
    carro := Carro{Marca: "Toyota", Modelo: "Corolla"}
    bike := Bicicleta{Tipo: "Mountain Bike"}

    ExibirVeiculo(carro)  // Funciona
    ExibirVeiculo(bike)   // Funciona também
}

Aqui, Carro e Bicicleta implementam todos os métodos de Veiculo, portanto ambas satisfazem a interface. A função ExibirVeiculo aceita qualquer Veiculo, demonstrando polimorfismo em Go.

Método String Customizado

Go oferece uma convenção especial: se você implementar o método String() em uma struct, ele será automaticamente chamado quando você tentar converter a struct para string (por exemplo, ao usar fmt.Println):

package main

import "fmt"

type Produto struct {
    Nome  string
    Preco float64
    Estoque int
}

// Implementando String para customizar a representação
func (p Produto) String() string {
    return fmt.Sprintf("Produto: %s | R$ %.2f | Estoque: %d", 
        p.Nome, 
        p.Preco, 
        p.Estoque)
}

func main() {
    produto := Produto{
        Nome:    "Notebook",
        Preco:   3500.00,
        Estoque: 15,
    }

    fmt.Println(produto)
    // Output: Produto: Notebook | R$ 3500.00 | Estoque: 15
}

Boas Práticas e Padrões Comuns

Construtor Pattern

Embora Go não force construtores, é comum criar funções que retornam uma instância inicializada corretamente. Essa é uma forma segura de garantir que a struct tenha valores sensatos:

package main

import (
    "fmt"
    "time"
)

type Pedido struct {
    ID        string
    Cliente   string
    Itens     []string
    CriadoEm  time.Time
    Completo  bool
}

// Função construtora (convenção: começar com "New")
func NewPedido(cliente string) *Pedido {
    return &Pedido{
        ID:       fmt.Sprintf("PED-%d", time.Now().UnixNano()),
        Cliente:  cliente,
        Itens:    make([]string, 0),
        CriadoEm: time.Now(),
        Completo: false,
    }
}

func (p *Pedido) AdicionarItem(item string) {
    p.Itens = append(p.Itens, item)
}

func (p *Pedido) Finalizar() {
    p.Completo = true
}

func main() {
    pedido := NewPedido("João Silva")
    pedido.AdicionarItem("Livro")
    pedido.AdicionarItem("Caneta")
    pedido.Finalizar()

    fmt.Printf("Pedido %s para %s: %v\n", 
        pedido.ID, 
        pedido.Cliente, 
        pedido.Itens)
}

Campos Privados e Métodos de Acesso

Campos privados (iniciados com letra minúscula) frequentemente são acessados através de métodos getter/setter, especialmente em structs que precisam validar dados:

package main

import (
    "fmt"
)

type ContaBancaria struct {
    titular string
    saldo   float64
}

// Getter
func (c *ContaBancaria) ObterSaldo() float64 {
    return c.saldo
}

// Getter
func (c *ContaBancaria) ObterTitular() string {
    return c.titular
}

// Setter com validação
func (c *ContaBancaria) Depositar(valor float64) error {
    if valor <= 0 {
        return fmt.Errorf("valor deve ser positivo")
    }
    c.saldo += valor
    return nil
}

func main() {
    conta := ContaBancaria{
        titular: "Maria",
        saldo:   1000.00,
    }

    fmt.Printf("Titular: %s, Saldo: R$ %.2f\n", 
        conta.ObterTitular(), 
        conta.ObterSaldo())

    conta.Depositar(500)
    fmt.Printf("Novo saldo: R$ %.2f\n", conta.ObterSaldo())
}

Evitar Embed Circular

Uma armadilha comum é criar dois tipos que se incorporam mutuamente, causando um erro de compilação. Go não permite isso. Se você precisa de relacionamento complexo, use campos nomeados ao invés de embedding:

package main

// ❌ ISSO NÃO COMPILA
// type A struct {
//  B
// }
// type B struct {
//  A
// }

// ✅ CORRETO: use campos nomeados
type Departamento struct {
    Nome      string
    Gerente   *Funcionario
}

type Funcionario struct {
    Nome         string
    Departamento *Departamento
}

func main() {
    // Inicialização com ponteiros
    dept := &Departamento{Nome: "TI"}
    func_emp := &Funcionario{
        Nome:         "Carlos",
        Departamento: dept,
    }
    dept.Gerente = func_emp

    println(func_emp.Nome, "trabalha em", func_emp.Departamento.Nome)
}

Referências

  1. Go Official Documentation - Structs — Documentação oficial sobre tipos struct em Go.

  2. Effective Go - Methods — Guia oficial sobre como escrever métodos idiomáticos em Go.

  3. Go by Example - Structs — Tutorial prático com exemplos interativos sobre structs.

  4. Alan Donovan & Brian Kernighan - The Go Programming Language — Livro clássico, capítulo 4 trata structs, métodos e interfaces em profundidade.

  5. Dave Cheney - Embedding in Go — Artigo fundamental explicando embedding e por que não é herança.


Artigos relacionados