O que é uma struct e quando usar
Uma struct é um tipo composto que agrupa campos (atributos) com nomes e tipos. Use structs para modelar entidades do seu domínio (por exemplo, Usuario, Pedido) e para transportar dados entre camadas do código (entrada/saída, persistência, APIs).
Em Go, structs são valores: ao atribuir uma struct para outra variável, você copia seus campos. Isso influencia como você inicializa, passa para funções e decide entre usar valor ou ponteiro.
Definindo structs: campos, tipos e composição
package dominio
type Usuario struct {
ID int
Nome string
Email string
}
Campos podem ser de qualquer tipo, inclusive outras structs. Uma forma comum de reutilização é a composição (embutir um tipo como campo anônimo), que também afeta como os campos são promovidos (acessados diretamente).
type Endereco struct {
Rua string
Cidade string
}
type Usuario struct {
ID int
Nome string
Email string
Endereco Endereco
}
Visibilidade: exportado vs não exportado (impacto em APIs de pacote)
Em Go, a visibilidade é definida pela primeira letra do identificador:
- Exportado: começa com letra maiúscula (visível fora do pacote). Ex.:
Usuario,Email. - Não exportado: começa com letra minúscula (visível apenas dentro do pacote). Ex.:
senhaHash,validar().
Isso afeta diretamente o design de APIs: se você expõe uma struct para outros pacotes, campos exportados podem ser lidos/alterados diretamente por quem usa seu pacote. Campos não exportados forçam o uso de construtores e métodos, permitindo invariantes e validações.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
Exemplo: protegendo invariantes com campos não exportados
package dominio
import "errors"
type Usuario struct {
ID int
Nome string
Email string
senhaHash string
}
func NovoUsuario(nome, email, senhaHash string) (Usuario, error) {
if nome == "" {
return Usuario{}, errors.New("nome obrigatorio")
}
if email == "" {
return Usuario{}, errors.New("email obrigatorio")
}
if senhaHash == "" {
return Usuario{}, errors.New("senhaHash obrigatorio")
}
return Usuario{Nome: nome, Email: email, senhaHash: senhaHash}, nil
}
func (u Usuario) SenhaHash() string {
return u.senhaHash
}
Note que quem está fora do pacote consegue ler SenhaHash(), mas não consegue alterar senhaHash diretamente.
Zero values: o estado inicial “padrão”
Quando você declara uma struct sem inicializar explicitamente, cada campo recebe seu zero value:
int→0string→""bool→false- ponteiros, slices, maps, interfaces, funcs →
nil - struct → todos os campos em zero value
var u Usuario
// u.ID == 0
// u.Nome == ""
// u.Email == ""
O zero value é útil para permitir que tipos tenham um estado inicial válido. Quando isso não for possível (por exemplo, um Pedido que precisa de itens), prefira construtores que garantam invariantes.
Literais de struct: nomeados vs posicionais
Literal nomeado (recomendado)
Você informa explicitamente quais campos está preenchendo. É mais legível e resistente a mudanças na ordem dos campos.
u := Usuario{
ID: 10,
Nome: "Ana",
Email: "ana@exemplo.com",
}
Também permite inicialização parcial (os demais campos ficam com zero value):
u := Usuario{Nome: "Ana"}
Literal posicional (use com cuidado)
Você fornece valores na ordem dos campos. Se a struct mudar (ex.: inserir um campo no meio), esse código pode quebrar ou, pior, compilar e ficar incorreto.
u := Usuario{10, "Ana", "ana@exemplo.com"}
Boas situações para literal posicional: structs pequenas, internas ao pacote, com poucos campos e baixa chance de mudança.
Padrões de inicialização: valor, ponteiro e construtores
Inicialização por valor
Boa para structs pequenas e imutáveis por convenção (você cria e não altera muito). Ao passar para funções, você copia.
func BoasVindas(u Usuario) string {
return "Ola, " + u.Nome
}
Inicialização com ponteiro
Útil quando você quer evitar cópias, precisa modificar o objeto em métodos, ou quer representar “ausência” com nil.
u := &Usuario{Nome: "Ana"}
func (u *Usuario) AtualizarEmail(email string) {
u.Email = email
}
Construtores (funções do tipo New/Novo)
Construtores ajudam a:
- validar dados na criação
- preencher defaults (valores padrão)
- manter campos não exportados
type Pedido struct {
ID int
Status string
Itens []ItemPedido
}
type ItemPedido struct {
SKU string
Quantidade int
PrecoCent int
}
func NovoPedido(id int) Pedido {
return Pedido{
ID: id,
Status: "criado",
Itens: []ItemPedido{},
}
}
Repare no padrão de inicializar Itens como slice vazia ([]ItemPedido{}) em vez de nil quando você quer garantir que iterações e serialização tenham um comportamento consistente.
Tags de struct: foco em JSON
Tags são metadados associados a campos, muito usados por bibliotecas de serialização. No pacote padrão encoding/json, a tag json controla o nome do campo no JSON e opções como omitempty.
type Usuario struct {
ID int `json:"id"`
Nome string `json:"nome"`
Email string `json:"email"`
}
Passo a passo: serializando e desserializando
package main
import (
"encoding/json"
"fmt"
)
type Usuario struct {
ID int `json:"id"`
Nome string `json:"nome"`
Email string `json:"email"`
}
func main() {
u := Usuario{ID: 1, Nome: "Ana", Email: "ana@exemplo.com"}
b, err := json.Marshal(u)
if err != nil {
panic(err)
}
fmt.Println(string(b))
var u2 Usuario
if err := json.Unmarshal(b, &u2); err != nil {
panic(err)
}
fmt.Printf("%+v\n", u2)
}
Cuidado com omitempty
omitempty faz o campo ser omitido do JSON quando estiver no zero value. Isso é útil para reduzir payload, mas pode ser perigoso quando o consumidor precisa distinguir “campo ausente” de “campo presente com valor zero”.
type AtualizacaoUsuario struct {
Nome string `json:"nome,omitempty"`
Email string `json:"email,omitempty"`
}
Problema comum: você quer permitir atualizar Quantidade para 0, mas 0 é zero value e o campo some do JSON com omitempty. Solução típica: usar ponteiros para representar “não informado”.
type PatchItem struct {
Quantidade *int `json:"quantidade,omitempty"`
}
Assim, nil significa “não veio”, e 0 (ponteiro para 0) significa “veio e é 0”.
Campos anônimos (embedded) e JSON
Campos anônimos promovem campos do tipo embutido. Em JSON, isso pode “achatar” a estrutura dependendo das tags e do tipo embutido.
type Auditoria struct {
CriadoEm string `json:"criado_em"`
}
type Pedido struct {
ID int `json:"id"`
Auditoria
}
Ao serializar, criado_em aparece no mesmo nível de id. Isso pode ser desejável, mas exige cuidado para evitar colisão de nomes e para manter clareza do contrato JSON.
Boas práticas ao modelar structs
- Prefira literais nomeados em código de aplicação e em APIs públicas.
- Exponha o mínimo necessário: campos não exportados + construtores/métodos para manter invariantes.
- Defina defaults explicitamente quando o zero value não representa um estado válido.
- Use ponteiros em patches (atualizações parciais) para diferenciar “ausente” de “zero”.
- Evite omitempty em campos onde o zero value é um valor significativo para o consumidor.
- Se usar campos anônimos, verifique colisões e clareza do JSON resultante.
Exercício prático: entidades Usuario e Pedido com validações simples
Objetivo
Criar duas entidades (Usuario e Pedido), com:
- construtores que validam dados obrigatórios
- métodos de validação simples
- tags JSON
- um exemplo de serialização
Regras
Usuario:NomeeEmailobrigatórios;Emaildeve conter@.Pedido: deve ter ao menos 1 item; cada item deve terSKUnão vazio,Quantidade > 0ePrecoCent > 0.
Implementação sugerida (passo a passo)
1) Defina as structs com tags JSON
package dominio
type Usuario struct {
ID int `json:"id"`
Nome string `json:"nome"`
Email string `json:"email"`
}
type ItemPedido struct {
SKU string `json:"sku"`
Quantidade int `json:"quantidade"`
PrecoCent int `json:"preco_cent"`
}
type Pedido struct {
ID int `json:"id"`
User Usuario `json:"usuario"`
Itens []ItemPedido `json:"itens"`
}
2) Crie funções de validação
package dominio
import (
"errors"
"strings"
)
func (u Usuario) Validar() error {
if strings.TrimSpace(u.Nome) == "" {
return errors.New("nome obrigatorio")
}
if strings.TrimSpace(u.Email) == "" {
return errors.New("email obrigatorio")
}
if !strings.Contains(u.Email, "@") {
return errors.New("email invalido")
}
return nil
}
func (p Pedido) Validar() error {
if err := p.User.Validar(); err != nil {
return err
}
if len(p.Itens) == 0 {
return errors.New("pedido deve ter ao menos 1 item")
}
for i, it := range p.Itens {
if strings.TrimSpace(it.SKU) == "" {
return errors.New("item " + string(rune('0'+i)) + ": sku obrigatorio")
}
if it.Quantidade <= 0 {
return errors.New("quantidade deve ser maior que zero")
}
if it.PrecoCent <= 0 {
return errors.New("preco_cent deve ser maior que zero")
}
}
return nil
}
Observação: para mensagens de erro por índice, você pode preferir fmt.Sprintf para formatar com clareza.
3) Crie construtores que aplicam defaults e validam
package dominio
func NovoUsuario(id int, nome, email string) (Usuario, error) {
u := Usuario{ID: id, Nome: nome, Email: email}
if err := u.Validar(); err != nil {
return Usuario{}, err
}
return u, nil
}
func NovoPedido(id int, user Usuario, itens []ItemPedido) (Pedido, error) {
p := Pedido{ID: id, User: user, Itens: itens}
if err := p.Validar(); err != nil {
return Pedido{}, err
}
return p, nil
}
4) Use as entidades e serialize para JSON
package main
import (
"encoding/json"
"fmt"
"log"
"seu_modulo/dominio"
)
func main() {
u, err := dominio.NovoUsuario(1, "Ana", "ana@exemplo.com")
if err != nil {
log.Fatal(err)
}
itens := []dominio.ItemPedido{
{SKU: "ABC-123", Quantidade: 2, PrecoCent: 1990},
{SKU: "XYZ-999", Quantidade: 1, PrecoCent: 5000},
}
p, err := dominio.NovoPedido(100, u, itens)
if err != nil {
log.Fatal(err)
}
b, err := json.MarshalIndent(p, "", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(b))
}
Desafios extras
- Adicionar
StatusaoPedidocom default"criado"no construtor e restringir valores válidos (ex.:criado,pago,enviado). - Criar um tipo
PatchUsuariocom ponteiros eomitemptypara simular atualização parcial via JSON. - Transformar
Pedido.Userem um campo não exportado e expor apenas métodos para leitura, simulando uma API mais restrita.