Structs
Structs hold data, methods are declared beside them, constructors are plain functions, and embedding replaces inheritance.
Section 05 of 31 7 min read
A struct is a typed collection of fields. There is no class, no constructor keyword and no this: you get a struct definition, an optional NewX() function by convention, and methods with an explicit receiver.
Defining a struct and creating instances
public class Message {
private final long id;
private final String topic;
private byte[] payload;
private int retries;
public Message(long id, String topic) {
this.id = id;
this.topic = topic;
this.retries = 0;
}
}
Message m = new Message(1L, "orders");type Message struct {
ID int64 // exported: other packages can read it
Topic string
Payload []byte // nil until set
Retries int // zero value 0 is a valid default
}
// Constructor by convention: an ordinary function
func NewMessage(id int64, topic string) *Message {
return &Message{ID: id, Topic: topic}
}
m := NewMessage(1, "orders")Initialisation styles
// 1. Named fields: always prefer this. Order does not matter,
// omitted fields get their zero value.
msg := Message{
ID: 1,
Topic: "orders.created",
}
// 2. Positional: breaks when a field is added. go vet warns
// when you do this with a struct from another package.
msg2 := Message{1, "orders.created", nil, 0}
// 3. Zero value: every field zeroed, ready to use
var msg3 Message
// Taking the address of a literal gives you a *Message
msg4 := &Message{ID: 2, Topic: "payments"}
// Anonymous struct: handy for one-off shapes in tests and JSON
point := struct{ X, Y int }{X: 1, Y: 2}Methods: explicit receivers
public class Message {
private int retries;
public boolean isRetryable() {
return this.retries < 3;
}
public void incrementRetry() {
this.retries++;
}
}// Value receiver: m is a copy of the caller's Message
func (m Message) IsRetryable() bool {
return m.Retries < 3
}
// Pointer receiver: m points at the caller's Message
func (m *Message) IncrementRetry() {
m.Retries++ // visible to the caller
}Struct equality
record Point(int x, int y) {} // equals/hashCode generated
new Point(1, 2).equals(new Point(1, 2)); // true
new Point(1, 2) == new Point(1, 2); // false: identity
// Usable as a HashMap key because of hashCode()type Point struct{ X, Y int }
Point{1, 2} == Point{1, 2} // true: field-by-field
// Comparable structs are valid map keys, no code needed
seen := map[Point]bool{}
seen[Point{1, 2}] = true
// Structs containing slices, maps or funcs are not comparable:
// == on them is a compile error.Embedding: composition instead of inheritance
class BaseMessage {
protected long id;
protected String topic;
public String logLine() {
return id + " on " + topic;
}
}
// PriorityMessage IS-A BaseMessage
class PriorityMessage extends BaseMessage {
private int priority;
}type BaseMessage struct {
ID int64
Topic string
}
func (b BaseMessage) LogLine() string {
return fmt.Sprintf("%d on %s", b.ID, b.Topic)
}
// PriorityMessage HAS-A BaseMessage
type PriorityMessage struct {
BaseMessage // no field name: embedded
Priority int
}
pm := PriorityMessage{
BaseMessage: BaseMessage{ID: 1, Topic: "alerts"},
Priority: 10,
}
pm.LogLine() // promoted: pm.BaseMessage.LogLine()
pm.ID // promoted: pm.BaseMessage.IDJava: PriorityMessage extends BaseMessage
Go: PriorityMessage embeds BaseMessage
pm.ID and pm.LogLine() work, but a PriorityMessage cannot be passed where a BaseMessage is expected.The trap for Java developers: no virtual dispatch through embedding
An outer type can declare a method with the same name as an embedded one; it shadows the promoted method. But this is not overriding. When a method on the embedded type calls another of its own methods, it always calls the embedded version, never the outer one. The Template Method pattern, where a base class calls a hook the subclass overrides, does not work through embedding.
If you need that kind of polymorphism, define an interface for the hook and pass an implementation in explicitly. That makes the extension point visible instead of implicit.
Struct tags: metadata for libraries
// Tags are string literals after a field. Libraries read them
// at runtime through reflection, much like Java annotations.
type User struct {
ID int64 `json:"id" db:"id"`
Email string `json:"email" validate:"required,email"`
Nickname string `json:"nickname,omitempty"` // omit when ""
CreatedAt time.Time `json:"created_at,omitzero"` // Go 1.24+
Password string `json:"-"` // never serialised
}
// json.Marshal output:
// {"id":1,"email":"ada@example.com","created_at":"2026-01-02T15:04:05Z"}