Skip to content
JavaGo
Part II · Types & Data

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

Java — class with a constructor
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");
Go — struct + constructor function
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

go — three ways to initialise, one to prefer
// 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

Java — methods inside the class, implicit 'this'
public class Message {
    private int retries;

    public boolean isRetryable() {
        return this.retries < 3;
    }

    public void incrementRetry() {
        this.retries++;
    }
}
Go — methods beside the struct, named receiver
// 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

Java — equals() and hashCode()
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()
Go — == compares every field
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

Java — extends (is-a)
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;
}
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
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.ID

Java: PriorityMessage extends BaseMessage

Go: PriorityMessage embeds BaseMessage

Fig. 5.1Inheritance makes a subtype; embedding does not. The embedded struct sits inside the outer one and its fields and methods are promoted, so 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

struct tags for JSON, databases and validation
// 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"}
↑ ↓ to navigateEnter to openEsc to close