Structs & Methods
Learning objectives
- Define structs and initialize them the platform way (field names, omit zero values).
- Attach methods and choose correctly between value and pointer receivers.
- Use embedding for composition and explain why Go has no inheritance.
- Read struct tags — the metadata that drives JSON, config, and database mapping.
Prerequisites
Time estimate
4 hours
Concepts
Structs and initialization
type Policy struct {
ID string
UserID string
ItemID string
Constraint map[string]any
ExpiresAt time.Time
}
// Platform style: field names, zero-value fields omitted, &T{} not new(T)
p := &Policy{
ID: uuid.NewString(),
UserID: userID,
ItemID: itemID,
}
Positional initialization (Policy{"a", "b", ...}) is banned by the linters for any struct with more than a couple of fields — it silently breaks when fields are added.
Methods and receivers
A method is a function with a receiver:
func (p *Policy) Expired(now time.Time) bool {
return !p.ExpiresAt.IsZero() && now.After(p.ExpiresAt)
}
Value receiver (func (p Policy)) gets a copy; pointer receiver (func (p *Policy)) can mutate and avoids copying. The decision rule:
- Does the method mutate the receiver? → pointer.
- Is the struct large, or does it contain a lock/pool/connection? → pointer.
- Otherwise a value receiver is fine — but don't mix: if any method needs a pointer receiver, give all methods pointer receivers.
The subtle trap: a value stored in a variable of interface type only exposes the value-receiver method set. If Expired has a pointer receiver, Policy{} (the value) does not satisfy an interface requiring Expired — &Policy{} does. You'll feel this in the next page on Interfaces.
Embedding — composition, not inheritance
Go has no extends. Instead a struct can embed another, promoting its fields and methods:
type Timestamps struct {
CreatedAt time.Time
UpdatedAt time.Time
}
type AccessRequest struct {
Timestamps // embedded — no field name
ID string
Status string
}
r := AccessRequest{}
_ = r.CreatedAt // promoted from Timestamps
This is composition with sugar: there's no polymorphic override, no super. When embedding a type with methods, those methods join your method set — which is how services embed shared config structs and mutexes. Embed judiciously: it's for is-composed-of, not for simulating class hierarchies.
Struct tags
Tags are string metadata on fields, read by libraries via reflection:
type ServerConfig struct {
Port int `mapstructure:"port" json:"port"`
Host string `mapstructure:"host" json:"host,omitempty"`
private string // unexported: invisible to json/mapstructure entirely
}
json:"..."— controls JSON field names,omitempty,-to skip.mapstructure:"..."— how viper binds YAML/env config to structs (Module 2, Configuration).db:"..."— how pgx maps rows to structs (Module 3, Database Patterns).
Only exported fields participate — a classic beginner bug is a struct that marshals to {} because its fields are lowercase.
Every DX domain entity is a plain struct: look at dx-acl-go/internal/domain/policy.go. Typed config embeds platform/config.Base and uses mapstructure-tagged fields. PostgreSQL row adapters use db tags with named-column scanning; keep those storage structs in the adapter rather than adding persistence tags to domain entities.
Exercises
- Model a
Dataset(id, name, provider, tags, createdAt) and aProvider; giveDatasetaString()method and anAge(now time.Time) time.Durationmethod. Choose receivers deliberately and justify them in comments. - Demonstrate the mixed-receiver interface trap: define
type Named interface{ Name() string }, implement it with a pointer receiver, and show which ofT{}/&T{}assignments compile. - Create a
Basestruct withID+ timestamps, embed it in two entities, and write one function that accepts anything with promotedID— notice you can't without an interface. Write the interface. - Round-trip a tagged struct through
encoding/json— include an unexported field and anomitemptyfield, and explain the output.
Check yourself
- Three reasons to choose a pointer receiver?
- Why must you not mix receiver kinds on one type?
- What does embedding promote, and what does it not do (compared to inheritance)?
- Why do unexported fields vanish from JSON?