A palavra-chave struct agrupa campos com nome num único tipo. Uma declaração type Order struct { ... } é a forma de descrever em Go uma encomenda, um utilizador, o corpo de um pedido HTTP ou um ficheiro de configuração. As structs não têm construtores nem herança. Acrescentas comportamento com métodos, reutilizas código incorporando uma struct noutra e controlas a codificação com struct tags (especificação do Go).
Resumo rápido
type Order struct { ... }declara um tipo struct. Cada campo tem um nome e um tipo, e os campos do mesmo tipo podem partilhar uma linha.- O valor zero de uma struct tem todos os campos no seu próprio valor zero, por isso
var o Orderestá pronto a usar sem qualquer preparação. - Escreve os literais compostos com os nomes dos campos, como em
Order{ID: 1001, Total: 4999}. Ogo vetassinala literais sem chaves de tipos de outros pacotes. - Só os campos exportados (com maiúscula) são visíveis para o
encoding/jsone para outros pacotes. Um campo em minúsculas é ignorado sem qualquer erro. - As struct tags como
json:"email,omitempty"controlam a codificação. Usaomitzero(Go 1.24) paratime.Timee outras structs, porque oomitemptynunca as omite. - Os métodos com recetor ponteiro podem alterar a struct. Os métodos com recetor de valor trabalham sobre uma cópia.
- Incorporar um tipo promove os seus campos e métodos para a struct exterior. É assim que o Go reutiliza código sem herança.
- As structs só são comparáveis com
==quando todos os campos o são. Atribuir uma struct copia-a, mas os slices e maps lá dentro continuam partilhados.
Como se declara uma struct em Go?
Escreve type, o nome, struct e os campos entre chavetas. Cada campo é um nome seguido de um tipo:
example.gogopackage main import ( "fmt" "time" ) type Order struct { ID int64 CustomerID int64 Status string Total int64 // cents CreatedAt time.Time } type Address struct { Street, City, Country string } func main() { var o Order fmt.Printf("%+v\n", o) fmt.Println(o.Status == "", o.CreatedAt.IsZero()) }
example.texttext{ID:0 CustomerID:0 Status: Total:0 CreatedAt:0001-01-01 00:00:00 +0000 UTC} true true
O Address mostra a forma curta. Os campos com o mesmo tipo podem partilhar uma linha, separados por vírgulas. Mesmo assim, a maioria das codebases põe um campo por linha, porque isso mantém os diffs pequenos e deixa espaço para uma tag ou um comentário.
O var o Order não precisa de construtor. Cada campo começa no seu valor zero: 0 para números, "" para strings, nil para ponteiros, slices e maps, e o time.Time zero para o CreatedAt. A palavra-chave var em Go explica os valores zero em detalhe.
Como qualquer tipo, uma struct pode ser declarada dentro de uma função quando só essa função precisa dela. A palavra-chave type em Go: tipos definidos, aliases e métodos mostra uma struct local loginRequest usada para descodificar um corpo JSON.
Como se cria um valor de uma struct?
Na maioria dos casos vais usar o valor zero com var, um literal composto com os nomes dos campos ou um ponteiro para um literal com &:
example.gogokeyed := Order{ID: 1001, CustomerID: 42, Status: "paid", Total: 4999} partial := Order{ID: 1002, Status: "pending"} positional := Order{1003, 42, "paid", 1250} p := &Order{ID: 1004} p.Status = "shipped"
example.texttext{ID:1001 CustomerID:42 Status:paid Total:4999} {ID:1002 CustomerID:0 Status:pending Total:0} {ID:1003 CustomerID:42 Status:paid Total:1250} {ID:1004 CustomerID:0 Status:shipped Total:0}
Um literal com chaves pode indicar qualquer subconjunto de campos, por qualquer ordem, e os restantes ficam com o seu valor zero. Um literal posicional tem de indicar todos os campos pela ordem da declaração, o que o torna frágil. Se acrescentares um campo Currency ao Order, todos os literais posicionais deixam de compilar, enquanto os literais com chaves continuam a funcionar.
Para tipos struct de outros pacotes, o go vet assinala os literais posicionais através do analisador composites:
example.gogoaddr := net.TCPAddr{net.ParseIP("10.0.0.5"), 5432, ""}
example.texttextmain.go:9:10: net.TCPAddr struct literal uses unkeyed fields
O p.Status funciona sobre um ponteiro sem teres de escrever (*p).Status. O Go desreferencia automaticamente um ponteiro para uma struct quando acedes a um campo.
Como se preenche um campo ponteiro opcional numa só linha?
Usa new com um valor, o que funciona desde o Go 1.26. Os campos ponteiro são a forma habitual de distinguir «não enviado» de «enviado como zero» no corpo de um pedido PATCH, e antes do Go 1.26 precisavas de uma variável temporária ou de uma função auxiliar para cada um:
example.gogotype UpdateUser struct { Name *string `json:"name,omitempty"` Email *string `json:"email,omitempty"` Age *int `json:"age,omitempty"` } func main() { patch := UpdateUser{ Email: new("[email protected]"), Age: new(31), } out, _ := json.Marshal(patch) fmt.Println(string(out)) }
example.texttext{"email":"[email protected]","age":31}
O new(31) aloca um int, guarda 31 nele e devolve o ponteiro (notas de lançamento do Go 1.26). Com go 1.25 no go.mod, o compilador recusa-o:
example.texttext./main.go:16:10: new("[email protected]") requires go1.26 or later (-lang was set to go1.25; check go.mod)
Porque é que os campos de struct em minúsculas não aparecem no JSON?
Porque não são exportados, e os outros pacotes não conseguem ver campos não exportados. O encoding/json é outro pacote, por isso ignora-os sem dar erro:
example.gogotype User struct { ID int64 Email string password string role string } func main() { u := User{ID: 7, Email: "[email protected]", password: "hunter2", role: "admin"} out, err := json.Marshal(u) fmt.Println(string(out), err) var in User err = json.Unmarshal([]byte(`{"ID":8,"Email":"[email protected]","role":"admin"}`), &in) fmt.Printf("%+v %v\n", in, err) }
example.texttext{"ID":7,"Email":"[email protected]"} <nil> {ID:8 Email:[email protected] password: role:} <nil>
As duas chamadas devolvem um erro nil, e o role da entrada é descartado sem qualquer aviso. Quando uma API em Go devolve {} ou um campo fica vazio depois de descodificar, verifica isto primeiro. A solução é pôr o campo com maiúscula e usar uma tag para manter o nome em minúsculas no JSON.
A mesma regra aplica-se a todos os pacotes que leem campos por reflexão: encoding/xml, os scanners do database/sql, o GORM, as bibliotecas de YAML e a execução de templates. Também podes tirar partido disto de propósito. Um campo não exportado é um sítio seguro para dados que nunca devem ser codificados, como uma ligação à base de dados ou uma cache.
O que são struct tags em Go?
Uma struct tag é um literal de string a seguir ao tipo de um campo. O próprio Go ignora-a, e os pacotes leem-na por reflexão para decidir como tratar esse campo. A tag JSON é a que vais escrever mais vezes:
example.gogotype User struct { ID int64 `json:"id"` Email string `json:"email"` Nickname string `json:"nickname,omitempty"` PasswordHash string `json:"-"` DeletedAt time.Time `json:"deleted_at,omitempty"` LastLoginAt time.Time `json:"last_login_at,omitzero"` } func main() { u := User{ID: 7, Email: "[email protected]", PasswordHash: "$2a$10$..."} enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") enc.Encode(u) }
example.texttext{ "id": 7, "email": "[email protected]", "deleted_at": "0001-01-01T00:00:00Z" }
Campo a campo, a saída explica-se assim:
json:"id"muda o nome do campo na saída.omitemptydescarta o campo quando éfalse,0,"",nilou um slice ou map vazio. ONicknameestá vazio, por isso desaparece.json:"-"nunca codifica o campo. OPasswordHashfica fora de todas as respostas.omitemptynão funciona com structs. ODeletedAté umtime.Timezero e continua a aparecer como0001-01-01T00:00:00Z, que os clientes tendem a interpretar como uma data real.omitzero, acrescentado no Go 1.24, descarta um campo quando tem o valor zero do seu tipo. Para tipos com um métodoIsZero() bool, como otime.Time, chama esse método. OLastLoginAtdesaparece (notas de lançamento do Go 1.24).
Em código novo, usa omitzero em campos struct e em time.Time. O omitempty continua a servir para strings, números, slices e maps.
Um campo pode ter tags para vários pacotes, separadas por espaços: `json:"email" db:"email" validate:"required,email"`. Cada pacote lê só a sua própria chave com reflect.StructTag.Get:
example.gogof, _ := reflect.TypeFor[User]().FieldByName("Nickname") fmt.Println(f.Tag.Get("json"))
example.texttextnickname,omitempty
Porque é que a minha struct tag é ignorada?
Normalmente por causa de um espaço ou de umas aspas em falta. O formato é rígido: key:"value", sem espaço depois dos dois pontos. json: "email" parece correto à primeira vista, mas o Tag.Get("json") devolve uma string vazia, e o campo é codificado como Email. O compilador aceita qualquer string como tag, por isso não se queixa. O go vet queixa-se, através do analisador structtag, e também apanha dois campos com o mesmo nome JSON:
example.gogotype User struct { Email string `json: "email"` Name string `json:"name" db:"name"` ID int64 `json:"id"` Ref int64 `json:"id"` }
example.texttextmain.go:4:2: struct field tag `json: "email"` not compatible with reflect.StructTag.Get: bad syntax for struct tag value main.go:7:2: struct field Ref repeats json tag "id" also at main.go:6
Os editores que usam o gopls mostram os mesmos avisos enquanto escreves.
Como funcionam os métodos nas structs?
Um método é uma função com um recetor, escrito antes do nome. O recetor é um valor (c Cart) ou um ponteiro (c *Cart), e essa escolha decide se o método pode alterar a struct:
example.gogotype Cart struct { Items []string Total int64 } func (c Cart) AddValue(item string, price int64) { c.Items = append(c.Items, item) c.Total += price } func (c *Cart) Add(item string, price int64) { c.Items = append(c.Items, item) c.Total += price } func main() { var c Cart c.AddValue("keyboard", 4999) fmt.Println(len(c.Items), c.Total) c.Add("keyboard", 4999) fmt.Println(len(c.Items), c.Total) }
example.texttext0 0 1 4999
O AddValue compila e corre, mas o carrinho continua vazio. Um recetor de valor é uma cópia da struct, por isso o método atualiza a cópia e depois deita-a fora. Este bug aparece muito em código Go novo, porque nenhum erro ou aviso aponta para ele. O Add recebe um ponteiro e altera o carrinho de quem o chama.
Não precisas de escrever (&c).Add(...). Quando o c é endereçável, o Go obtém o endereço por ti. O inverso também funciona: um ponteiro pode chamar métodos com recetor de valor.
Usa um recetor ponteiro quando o método altera a struct, quando a struct é grande ou quando contém um sync.Mutex ou um campo semelhante que não pode ser copiado. Usa um recetor de valor para valores pequenos e imutáveis, como um Money ou um Point. Se algum método de um tipo precisar de um recetor ponteiro, dá recetores ponteiro a todos. Misturar os dois torna o conjunto de métodos mais difícil de perceber, porque só o *Cart passa a ter todos os métodos, e só o *Cart satisfaz as interfaces que precisam de todos eles (especificação do Go).
Como funciona a incorporação de structs em Go?
Um campo escrito só com um tipo e sem nome é um campo incorporado (embedded). Os seus campos e métodos são promovidos, o que significa que os podes usar como se estivessem declarados na struct exterior:
example.gogotype Timestamps struct { CreatedAt time.Time UpdatedAt time.Time } func (t *Timestamps) Touch(now time.Time) { if t.CreatedAt.IsZero() { t.CreatedAt = now } t.UpdatedAt = now } type Order struct { ID int64 Timestamps } type User struct { ID int64 Email string Timestamps } func main() { now := time.Date(2026, 9, 26, 9, 0, 0, 0, time.UTC) var o Order o.Touch(now) fmt.Println(o.CreatedAt.Format(time.DateOnly), o.Timestamps.UpdatedAt.Format(time.Kitchen)) u := User{ID: 7, Email: "[email protected]", Timestamps: Timestamps{CreatedAt: now}} fmt.Println(u.CreatedAt.Year()) }
example.texttext2026-09-26 9:00AM 2026
O o.Touch(now) é uma forma abreviada de o.Timestamps.Touch(now). O campo incorporado continua a existir com o nome do seu tipo, e é também assim que o preenches num literal: Timestamps: Timestamps{...}. O encoding/json também achata as structs incorporadas, por isso o Order acima é codificado como {"ID":0,"CreatedAt":"2026-09-26T09:00:00Z","UpdatedAt":"2026-09-26T09:00:00Z"}.
Incorporação não é herança. Um Order não é um Timestamps, e não podes passar um Order a uma função que recebe um Timestamps. Quando o Touch corre, o seu recetor é o Timestamps interior. Não tem forma de chegar ao Order que o rodeia nem de chamar métodos que o Order define. Uma subclasse em Java ou Python consegue fazê-lo, e é daí que vêm as perguntas do tipo «que override é que corre?». Em Go, essas perguntas não se põem.
Quando a struct exterior declara um campo ou método com o mesmo nome, é esse que prevalece, e o incorporado continua acessível através do nome do tipo:
example.gogotype Base struct{ ID int64 } func (Base) Describe() string { return "base" } type Order struct { Base ID string } func (Order) Describe() string { return "order" } func main() { o := Order{Base: Base{ID: 42}, ID: "ord_42"} fmt.Println(o.ID, o.Base.ID) fmt.Println(o.Describe(), o.Base.Describe()) }
example.texttextord_42 42 order base
Podes incorporar uma interface numa struct?
Sim, e é uma forma comum de embrulhar um tipo substituindo apenas um método. Um middleware HTTP que regista o código de estado incorpora o http.ResponseWriter, fica com o Header e o Write sem esforço e substitui só o WriteHeader:
example.gogotype statusRecorder struct { http.ResponseWriter status int } func (r *statusRecorder) WriteHeader(code int) { r.status = code r.ResponseWriter.WriteHeader(code) } func logStatus(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK} next.ServeHTTP(rec, req) slog.Info("request", "path", req.URL.Path, "status", rec.status) }) }
example.texttext2026/09/26 09:04:44 INFO request path=/orders/99 status=404
Como o *statusRecorder tem os três métodos, satisfaz o http.ResponseWriter e pode ser passado ao handler seguinte. Se a interface incorporada for nil, chamar um dos seus métodos causa um panic, por isso preenche-a sempre no literal.
Deves incorporar um sync.Mutex numa struct?
Não num tipo exportado. A incorporação promove o Lock e o Unlock para a API pública do tipo, por isso qualquer chamador pode bloquear a tua cache a partir de fora:
example.gogotype Cache struct { sync.Mutex items map[string]string }
Um campo com nome e não exportado mantém o lock como um detalhe de implementação:
example.gogotype SafeCache struct { mu sync.Mutex items map[string]string } func (c *SafeCache) Get(key string) (string, bool) { c.mu.Lock() defer c.mu.Unlock() v, ok := c.items[key] return v, ok }
O valor zero de um sync.Mutex é um mutex desbloqueado, por isso o SafeCache não precisa de preparação para o lock. Mesmo assim, o map tem de ser criado antes da primeira escrita.
Podes comparar e copiar structs em Go?
Podes comparar duas structs com == quando todos os campos são comparáveis. A comparação é feita campo a campo. Isso faz das structs pequenas bons tipos de valor e boas chaves de map:
example.gogotype Money struct { Amount int64 Currency string } type RouteKey struct { Method string Path string } func main() { fmt.Println(Money{4999, "EUR"} == Money{4999, "EUR"}) hits := map[RouteKey]int{} hits[RouteKey{"GET", "/orders"}]++ hits[RouteKey{"GET", "/orders"}]++ hits[RouteKey{"POST", "/orders"}]++ fmt.Println(hits[RouteKey{"GET", "/orders"}], len(hits)) }
example.texttexttrue 2 2
Uma chave composta como RouteKey é mais limpa do que colar strings na forma "GET /orders", e não pode colidir quando um caminho contém um espaço. Aqui os literais posicionais não têm problema, porque o RouteKey é declarado no mesmo pacote e tem dois campos óbvios.
Slices, maps e funções não são comparáveis, por isso uma struct que contenha um deles também não é. O compilador avisa-te:
example.gogotype Order struct { ID int64 Items []string } func main() { a := Order{ID: 1} b := Order{ID: 1} fmt.Println(a == b) }
example.texttext./main.go:13:14: invalid operation: a == b (struct containing []string cannot be compared)
Compara essas structs campo a campo, com slices.Equal para o slice, ou com reflect.DeepEqual nos testes.
Atribuir uma struct copia-a?
Sim, mas a cópia é superficial. Todos os campos são copiados, e um campo slice ou map é copiado como um cabeçalho que continua a apontar para os mesmos dados:
example.gogotype Order struct { ID int64 Items []string Meta map[string]string } func main() { original := Order{ID: 1, Items: []string{"keyboard"}, Meta: map[string]string{"source": "web"}} copied := original copied.ID = 2 copied.Items[0] = "mouse" copied.Meta["source"] = "api" fmt.Println(original.ID, original.Items, original.Meta) }
example.texttext1 [mouse] map[source:api]
Alterar o copied.ID não mexeu no original. Alterar um elemento de copied.Items ou uma chave de copied.Meta alterou também o original. Para obteres uma cópia independente, clona tu mesmo esses campos com slices.Clone e maps.Clone.
A mesma cópia acontece quando passas uma struct por valor a uma função. Numa struct que contém um sync.Mutex, essa cópia é um bug, porque a cópia fica com o seu próprio lock. O go vet assinala-o através do analisador copylocks:
example.gogotype Counter struct { mu sync.Mutex hits int } func report(c Counter) { fmt.Println(c.hits) }
example.texttextmain.go:13:15: report passes lock by value: shop.Counter contains sync.Mutex main.go:19:9: call of report copies lock value: shop.Counter contains sync.Mutex
A solução é func report(c *Counter).
O que é uma struct anónima em Go?
É um tipo struct escrito diretamente no sítio onde é usado, sem nome. Usa-se quando uma estrutura só é precisa uma vez. Os dois sítios onde mais a vais ver são os testes orientados por tabelas e a descodificação de parte de uma resposta JSON:
example.gogovar resp struct { Data struct { Status string `json:"status"` } `json:"data"` } body := `{"data":{"status":"shipped","carrier":"DHL"},"meta":{"request_id":"abc"}}` if err := json.Unmarshal([]byte(body), &resp); err != nil { panic(err) } fmt.Println(resp.Data.Status) tests := []struct { name string input string want string }{ {"trims spaces", " [email protected] ", "[email protected]"}, {"already clean", "[email protected]", "[email protected]"}, } for _, tt := range tests { got := strings.ToLower(strings.TrimSpace(tt.input)) fmt.Println(tt.name, got == tt.want) }
example.texttextshipped trims spaces true already clean true
O descodificador preenche só os campos que a struct declara, por isso o carrier e o meta são ignorados. Não precisas de um tipo com nome para uma resposta da qual só lês um campo. Assim que a mesma estrutura aparecer em dois sítios, dá-lhe um nome.
Para que serve o struct{}?
O struct{} é uma struct sem campos. Ocupa zero bytes, por isso é o tipo de valor preferido quando só interessa a presença de alguma coisa. Os dois casos comuns são um conjunto construído sobre um map e um canal usado só para sinalizar:
example.gogoseen := map[string]struct{}{} for _, id := range []string{"evt_1", "evt_2", "evt_1"} { if _, dup := seen[id]; dup { fmt.Println("skipping duplicate", id) continue } seen[id] = struct{}{} } fmt.Println(len(seen), unsafe.Sizeof(struct{}{})) done := make(chan struct{}) go func() { defer close(done) }() <-done fmt.Println("worker finished")
example.texttextskipping duplicate evt_1 2 0 worker finished
O map[string]struct{} diz «conjunto de strings» com mais clareza do que o map[string]bool, em que quem lê tem de se perguntar o que significa false. O chan struct{} diz que nenhum dado passa pelo canal, só o facto de ter sido fechado. O ctx.Done() devolve um <-chan struct{} pela mesma razão.
A ordem dos campos muda o tamanho de uma struct?
Sim. Cada campo é alinhado ao seu tamanho, por isso o compilador insere preenchimento (padding) entre um campo pequeno e um maior que venha a seguir. Os mesmos cinco campos ocupam o dobro da memória na ordem errada numa máquina de 64 bits:
example.gogotype EventLoose struct { Active bool ID int64 Retried bool Attempts int32 Urgent bool } type EventPacked struct { ID int64 Attempts int32 Active bool Retried bool Urgent bool } func main() { fmt.Println(unsafe.Sizeof(EventLoose{}), unsafe.Sizeof(EventPacked{})) }
example.texttext32 16
O EventLoose acrescenta 7 bytes de preenchimento depois do Active para que o ID comece num limite de 8 bytes, e depois mais alguns a seguir ao Retried e ao Urgent. O EventPacked ordena os campos do maior para o mais pequeno e desperdiça só 1 byte no fim.
Na maior parte das structs isto não tem importância, e agrupar os campos pelo significado é mais legível. Começa a ter importância quando guardas milhões de valores num slice ou numa cache. O analisador fieldalignment do golang.org/x/tools assinala as structs que podiam ser mais pequenas.
Struct em Go vs classe: qual é a diferença?
Uma struct em Go guarda dados e pode ter métodos, o que cobre a maior parte do que uma classe faz em Java, C# ou Python. O resto é diferente de propósito:
| Struct em Go | Classe (Java, C#, Python) | |
|---|---|---|
| Construtor | Nenhum. O valor zero, ou uma função NewX por convenção | Método construtor especial |
| Reutilização | Incorporação (composição) | Herança, mais composição |
| Polimorfismo | Interfaces, satisfeitas implicitamente | Classes base e interfaces declaradas |
| Visibilidade | Por pacote, pela maiúscula inicial | public, private, protected por membro |
| Métodos | Declarados fora do tipo, em qualquer sítio do pacote | Declarados dentro do corpo da classe |
| Semântica de cópia | Valor por omissão, & para um ponteiro | Referência por omissão |
Quando uma struct precisa de validação ou de valores por omissão, escreve uma função simples que a devolva. A convenção é NewX, e é normal devolver também um erro:
example.gogotype Client struct { baseURL string http *http.Client retries int } func NewClient(baseURL string) (*Client, error) { if baseURL == "" { return nil, errors.New("baseURL is required") } return &Client{ baseURL: baseURL, http: &http.Client{Timeout: 10 * time.Second}, retries: 3, }, nil }
Com campos não exportados, quem está fora do pacote só consegue obter um Client através do NewClient, com os valores por omissão aplicados. Como o Go não tem super nem overrides, nunca tens de subir por uma hierarquia de classes para perceber o que um método faz. O comportamento de uma struct são os seus próprios métodos mais os métodos que incorpora, e podes lê-los todos num só pacote. Isso ajuda muito quando uma codebase em Go cresce.
Onde entra o LevelUpGo
O LevelUpGo ensina Go através de exercícios que executam código Go real no navegador. O Composite Types constrói passo a passo a tua primeira struct, funções construtoras, métodos e incorporação, e depois usa-os num pequeno projeto. O Pointers & Memory aborda ponteiros para structs, o new e os recetores de valor vs ponteiro, e termina com um armazenamento de feature flags que altera structs no próprio sítio. O Interfaces & Polymorphism mostra como as structs satisfazem interfaces sem o declararem. O Training Ground tem exercícios curtos e independentes para praticares fora de um curso. Para as outras 24 palavras reservadas, vê Palavras-chave do Go: as 25 explicadas.
Perguntas frequentes
O struct é uma palavra-chave em Go?
Sim. O struct é uma das 25 palavras-chave reservadas do Go, por isso não o podes usar como nome de variável ou de função. Inicia um tipo struct, que quase sempre recebe um nome com type, como em type Order struct { ... }. Também aparece nas structs anónimas e na struct vazia struct{}.
O Go tem classes?
Não. Em vez disso, o Go tem structs com métodos, interfaces e incorporação. Uma struct guarda os dados, os métodos acrescentam comportamento, as interfaces dão polimorfismo e a incorporação reutiliza código através de composição. Não há herança nem uma palavra-chave para construtores. Por convenção, uma função NewX faz esse papel.
Devo usar um recetor de valor ou um recetor ponteiro?
Usa um recetor ponteiro se o método altera a struct, se a struct é grande ou se contém um sync.Mutex. Usa um recetor de valor para valores pequenos que nunca mudam, como um Money ou um Point. Se um método de um tipo precisar de um recetor ponteiro, faz com que todos sejam recetores ponteiro, para que o tipo se comporte de forma consistente.
Porque é que o json.Marshal devolve um objeto vazio para a minha struct?
Porque os campos da struct não são exportados. O encoding/json só vê campos que começam com maiúscula e ignora os outros sem dar erro. Muda o nome de email para Email e acrescenta `json:"email"` para manter o nome em minúsculas no JSON.
Qual é a diferença entre omitempty e omitzero?
O omitempty descarta false, 0, "", nil e slices e maps vazios, mas nunca descarta uma struct, por isso um time.Time zero continua a aparecer como 0001-01-01T00:00:00Z. O omitzero, acrescentado no Go 1.24, descarta qualquer campo que tenha o valor zero do seu tipo e usa o método IsZero do tipo quando ele existe.
Fontes
- The Go Programming Language Specification, Struct types: https://go.dev/ref/spec#Struct_types
- The Go Programming Language Specification, Composite literals: https://go.dev/ref/spec#Composite_literals
- The Go Programming Language Specification, Method sets: https://go.dev/ref/spec#Method_sets
- The Go Programming Language Specification, Comparison operators: https://go.dev/ref/spec#Comparison_operators
- encoding/json, Marshal: https://pkg.go.dev/encoding/json#Marshal
- reflect, StructTag: https://pkg.go.dev/reflect#StructTag
- Go 1.24 Release Notes, omitzero: https://go.dev/doc/go1.24
- Go 1.26 Release Notes, new with an expression: https://go.dev/doc/go1.26
- go vet analyzers (composites, structtag, copylocks): https://pkg.go.dev/cmd/vet
