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 belowfunc 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 mattermsg := 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 valuevar 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 addressmsg4 := &Message{ID: 2, Topic: "payments"}// msg4 is *Message — a pointer to a Message
Methods — 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 originalfunc (m Message) IsRetryable() bool { return m.Retries < 3 // m is a copy — changes here are discarded}// Pointer receiver — m is the REAL struct, can mutatefunc (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 Userclass 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.ID
Why 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 themtype 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