Structs: modelando dados sem classes

1. Introdução às Structs em Go

Go não possui classes. Essa afirmação pode soar estranha para quem vem de linguagens como Java, Python ou C#, mas é uma escolha deliberada de design. Em vez do paradigma clássico de orientação a objetos com herança, polimorfismo por subclasse e encapsulamento rígido, Go oferece structs — agregados de dados que podem ser combinados com métodos para criar comportamentos.

Uma struct em Go é um tipo composto que agrupa zero ou mais campos nomeados, cada um com seu próprio tipo. Enquanto classes definem tanto estado quanto comportamento em uma unidade monolítica, structs são fundamentalmente estruturas de dados que podem opcionalmente ter métodos associados. Essa distinção é sutil, mas poderosa: structs incentivam a separação entre dados e comportamento, promovendo composição sobre herança.

type Pessoa struct {
    Nome  string
    Idade int
    Email string
}

2. Declaração e Inicialização de Structs

A declaração de uma struct segue a sintaxe type Nome struct { campos }. Existem várias formas de inicializar uma struct:

// Declaração
type Endereco struct {
    Rua    string
    Numero int
    Cidade string
}

// Inicialização literal nomeada (recomendada)
e1 := Endereco{
    Rua:    "Av. Paulista",
    Numero: 1000,
    Cidade: "São Paulo",
}

// Inicialização literal posicional (frágil, não recomendada)
e2 := Endereco{"Rua Augusta", 500, "São Paulo"}

// Inicialização com var (todos os campos com zero values)
var e3 Endereco

// Usando new() retorna um ponteiro para a struct com zero values
e4 := new(Endereco)

// Equivalente ao new, mas mais idiomático
e5 := &Endereco{}

A inicialização nomeada é preferível por ser explícita e resiliente a mudanças na ordem dos campos. Structs vazias (struct{}) são úteis como valores de sinalização ou em canais.

3. Acessando e Modificando Campos

O operador ponto (.) é usado para acessar e modificar campos:

p := Pessoa{Nome: "João", Idade: 30}
fmt.Println(p.Nome) // João
p.Idade = 31

Um aspecto crucial é que structs em Go são tipos valor. Atribuir uma struct a outra variável ou passá-la para uma função cria uma cópia completa:

func envelhecer(p Pessoa) {
    p.Idade++
}

p1 := Pessoa{Nome: "Maria", Idade: 25}
envelhecer(p1)
fmt.Println(p1.Idade) // 25 — não foi modificada!

Para modificar a struct original, usamos ponteiros:

func envelhecer(p *Pessoa) {
    p.Idade++ // Go permite acesso direto sem desreferência explícita
}

p2 := &Pessoa{Nome: "Maria", Idade: 25}
envelhecer(p2)
fmt.Println(p2.Idade) // 26

Go automaticamente desreferencia ponteiros para structs quando usamos o operador ponto, tornando o código mais limpo.

4. Structs Aninhadas e Composição

Structs podem conter outras structs como campos, criando hierarquias de dados:

type Usuario struct {
    Nome     string
    Endereco Endereco // aninhamento explícito
}

u := Usuario{
    Nome: "Carlos",
    Endereco: Endereco{
        Rua:    "Rua das Flores",
        Numero: 123,
        Cidade: "Curitiba",
    },
}
fmt.Println(u.Endereco.Cidade) // Curitiba

Go também suporta composição com campos anônimos (embedding), que promove campos e métodos da struct embutida:

type Animal struct {
    Nome  string
    Som   string
}

func (a Animal) EmitirSom() {
    fmt.Println(a.Som)
}

type Cachorro struct {
    Animal          // campo anônimo (embedding)
    Raca   string
}

c := Cachorro{
    Animal: Animal{Nome: "Rex", Som: "Au au"},
    Raca:   "Labrador",
}

// Campos promovidos
fmt.Println(c.Nome) // Rex (promovido de Animal)
c.EmitirSom()       // Au au (método promovido)

Se houver conflito de nomes entre a struct externa e a embutida, o campo da struct externa tem precedência.

5. Métodos em Structs

Métodos são funções com um receiver especial que as associa a um tipo:

type Retangulo struct {
    Largura, Altura float64
}

// Receiver por valor
func (r Retangulo) Area() float64 {
    return r.Largura * r.Altura
}

// Receiver por ponteiro
func (r *Retangulo) Escalar(fator float64) {
    r.Largura *= fator
    r.Altura *= fator
}

Quando usar cada receiver:
- Receiver por valor: quando o método não modifica a struct e a struct é pequena (poucos campos)
- Receiver por ponteiro: quando o método precisa modificar a struct, ou a struct é grande (evita cópia desnecessária)

Um método especial é String(), que implementa a interface Stringer do pacote fmt:

func (p Pessoa) String() string {
    return fmt.Sprintf("%s (%d anos)", p.Nome, p.Idade)
}

p := Pessoa{"Ana", 28}
fmt.Println(p) // Ana (28 anos)

6. Tags de Struct e Reflexão

Tags são metadados anexados aos campos de uma struct, usados principalmente para serialização e validação:

type Produto struct {
    ID    int     `json:"id" validate:"required"`
    Nome  string  `json:"nome" validate:"required,min=3"`
    Preco float64 `json:"preco" validate:"gt=0"`
    Ativo bool    `json:"ativo"`
}

O pacote reflect permite ler essas tags em tempo de execução:

import "reflect"

func lerTags(s interface{}) {
    t := reflect.TypeOf(s)
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        fmt.Printf("Campo: %s, JSON: %s\n", field.Name, field.Tag.Get("json"))
    }
}

lerTags(Produto{})
// Campo: ID, JSON: id
// Campo: Nome, JSON: nome
// Campo: Preco, JSON: preco
// Campo: Ativo, JSON: ativo

Bibliotecas como encoding/json e validator usam intensamente tags para configurar comportamento automático.

7. Structs vs. Outras Abordagens

Structs em Go substituem classes com algumas diferenças fundamentais:

  • Sem herança: usa-se composição (embedding) em vez de herança
  • Sem construtores: o padrão é usar funções construtoras (NewStruct())
  • Zero values: toda struct declarada tem valores padrão para seus campos

Padrão construtor idiomático:

type Config struct {
    Host string
    Port int
    Debug bool
}

func NewConfig(host string, port int) *Config {
    if host == "" {
        host = "localhost"
    }
    if port == 0 {
        port = 8080
    }
    return &Config{
        Host:  host,
        Port:  port,
        Debug: false,
    }
}

Quando usar structs vs. outras estruturas:
- Structs: dados com campos nomeados e tipos fixos (modelagem de domínio)
- Maps: dados dinâmicos ou quando as chaves são desconhecidas em tempo de compilação
- Slices: coleções homogêneas de elementos

Structs oferecem segurança de tipo e desempenho superior, enquanto maps fornecem flexibilidade. A escolha depende do contexto e dos requisitos de cada aplicação.


Referências