Voltar ao blog

A palavra-chave struct em Go: campos, tags, incorporação e métodos

Como funciona a palavra-chave struct do Go: declaração de campos, valores zero e literais compostos, campos exportados e JSON, struct tags com omitempty e omitzero, recetores de valor vs ponteiro, incorporação, comparação e cópia, a struct vazia e a ordem dos campos.

A palavra-chave struct em Go: campos, tags, incorporação e métodos

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 Order está pronto a usar sem qualquer preparação.
  • Escreve os literais compostos com os nomes dos campos, como em Order{ID: 1001, Total: 4999}. O go vet assinala literais sem chaves de tipos de outros pacotes.
  • Só os campos exportados (com maiúscula) são visíveis para o encoding/json e para outros pacotes. Um campo em minúsculas é ignorado sem qualquer erro.
  • As struct tags como json:"email,omitempty" controlam a codificação. Usa omitzero (Go 1.24) para time.Time e outras structs, porque o omitempty nunca 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.gogo
package 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.gogo
keyed := 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.gogo
addr := net.TCPAddr{net.ParseIP("10.0.0.5"), 5432, ""}
example.texttext
main.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.gogo
type 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.gogo
type 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.gogo
type 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.
  • omitempty descarta o campo quando é false, 0, "", nil ou um slice ou map vazio. O Nickname está vazio, por isso desaparece.
  • json:"-" nunca codifica o campo. O PasswordHash fica fora de todas as respostas.
  • omitempty não funciona com structs. O DeletedAt é um time.Time zero e continua a aparecer como 0001-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étodo IsZero() bool, como o time.Time, chama esse método. O LastLoginAt desaparece (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.gogo
f, _ := reflect.TypeFor[User]().FieldByName("Nickname")
fmt.Println(f.Tag.Get("json"))
example.texttext
nickname,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.gogo
type User struct {
	Email string `json: "email"`
	Name  string `json:"name" db:"name"`
	ID    int64  `json:"id"`
	Ref   int64  `json:"id"`
}
example.texttext
main.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.gogo
type 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.texttext
0 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.gogo
type 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.texttext
2026-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.gogo
type 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.texttext
ord_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.gogo
type 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.texttext
2026/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.gogo
type 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.gogo
type 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.gogo
type 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.texttext
true
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.gogo
type 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.gogo
type 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.texttext
1 [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.gogo
type Counter struct {
	mu   sync.Mutex
	hits int
}

func report(c Counter) {
	fmt.Println(c.hits)
}
example.texttext
main.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.gogo
var 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.texttext
shipped
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.gogo
seen := 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.texttext
skipping 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.gogo
type 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.texttext
32 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 GoClasse (Java, C#, Python)
ConstrutorNenhum. O valor zero, ou uma função NewX por convençãoMétodo construtor especial
ReutilizaçãoIncorporação (composição)Herança, mais composição
PolimorfismoInterfaces, satisfeitas implicitamenteClasses base e interfaces declaradas
VisibilidadePor pacote, pela maiúscula inicialpublic, private, protected por membro
MétodosDeclarados fora do tipo, em qualquer sítio do pacoteDeclarados dentro do corpo da classe
Semântica de cópiaValor por omissão, & para um ponteiroReferê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.gogo
type 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

Escreve Go como um engenheiro sénior

Lições interativas no navegador. As primeiras são grátis.

Experimenta uma lição grátisOu cria uma conta gratuita