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
-
Go Official Documentation - Structs — Documentação oficial sobre tipos struct em Go.
-
Effective Go - Methods — Guia oficial sobre como escrever métodos idiomáticos em Go.
-
Go by Example - Structs — Tutorial prático com exemplos interativos sobre structs.
-
Alan Donovan & Brian Kernighan - The Go Programming Language — Livro clássico, capítulo 4 trata structs, métodos e interfaces em profundidade.
-
Dave Cheney - Embedding in Go — Artigo fundamental explicando embedding e por que não é herança.