Section 03
Structs
Structs hold data. Methods are attached separately. There is no class, no constructor keyword, no this — just a struct definition, a NewX() factory function, and methods with explicit receivers.
Defining a struct and creating instances
Java — class with constructor
public class Message {
private long id;
private String topic;
private byte[] payload;
private int retries;
public Message(long id, String topic) {
this.id = id;
this.topic = topic;
this.retries = 0; // explicit default
}
}
Message m = new Message(1L, "orders");Go — struct + factory function
type Message struct {
ID int64 // exported — other packages can read
Topic string // exported
Payload []byte // exported — byte slice, nil until set
Retries int // exported — zero value is 0, valid default
}
// Factory function — Go's 'constructor'
// Returns value (stack) — pointer version below
func NewMessage(id int64, topic string) Message {
return Message{ID: id, Topic: topic}
// Retries gets zero value 0 automatically
}
m := NewMessage(1, "orders")Why Go does thisStructs have no constructors.
NewX() is a convention, not a language feature. This means you can also create structs inline without going through a constructor — which is often cleaner for test data and simple cases.Struct initialisation styles
go — three ways to initialise
// 1. Named fields — always prefer this, order does not matter
msg := Message{
ID: 1,
Topic: "orders.created",
// Payload omitted — gets nil (zero value for []byte)
// Retries omitted — gets 0
}
// 2. Positional — fragile, breaks when struct gains fields. Avoid.
msg2 := Message{1, "orders.created", nil, 0}
// 3. Zero value — every field gets its zero value
var msg3 Message // Message{ID:0, Topic:"", Payload:nil, Retries:0}
// Pointer to struct — most common in service/engine code
// & allocates on the heap and returns the address
msg4 := &Message{ID: 2, Topic: "payments"}
// msg4 is *Message — a pointer to a MessageMethods — receiver syntax
Java — methods inside class, implicit 'this'
public class Message {
private int retries;
// 'this' is implicit
public boolean isRetryable() {
return this.retries < 3;
}
// mutates this
public void incrementRetry() {
this.retries++;
}
}Go — methods outside struct, explicit receiver
// Methods live OUTSIDE the struct definition
// The receiver (m Message) is explicit — it IS 'this'
// Value receiver — m is a COPY, cannot mutate original
func (m Message) IsRetryable() bool {
return m.Retries < 3
// m is a copy — changes here are discarded
}
// Pointer receiver — m is the REAL struct, can mutate
func (m *Message) IncrementRetry() {
m.Retries++ // mutates the original
}Why Go does thisGo makes mutation visible. If you see
(m Message), this method cannot change anything. If you see (m *Message), it can. In Java, every non-static method can mutate the object — you have to read the body to know. In Go, the signature tells you.Embedding — composition over inheritance
Java — extends (is-a relationship)
class BaseMessage {
protected long id;
protected String topic;
public String logLine() {
return id + " on " + topic;
}
}
// Admin IS-A User
class PriorityMessage extends BaseMessage {
private int priority;
// inherits id, topic, logLine()
}Go — embedding (has-a, with promotion)
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 (embedded)
type PriorityMessage struct {
BaseMessage // no field name = embedded
Priority int
}
pm := PriorityMessage{
BaseMessage: BaseMessage{ID: 1, Topic: "alerts"},
Priority: 10,
}
pm.LogLine() // promoted — same as pm.BaseMessage.LogLine()
pm.ID // promoted field — same as pm.BaseMessage.IDWhy Go does thisGo has no inheritance hierarchy. Embedding is mechanical field/method promotion — not an is-a relationship. There is no polymorphism through embedding (that comes from interfaces). This eliminates entire categories of OOP complexity: no diamond problem, no method hiding, no abstract classes.
Struct tags — metadata for libraries
struct tags — JSON, DB, validation
// Tags are string literals in backticks after field declarations
// Libraries like encoding/json and validator packages read them
type User struct {
ID int64 `json:"id" db:"id"`
Email string `json:"email" db:"email" validate:"required,email"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
Password string `json:"-"` // json:"-" = omit from JSON output
}
// json.Marshal uses the json tag for key names:
// { "id": 1, "email": "h@x.com", "created_at": "..." }
// Password is not included because json:"-"
// easyjson can generate MarshalJSON()/UnmarshalJSON() from these tags
// sqlc is different: it generates code from SQL files + schema, not struct tags
// go-playground/validator uses validate tags