Syntax at a glance

Types come after names, there are no semicolons, and nothing has to live in a class.

package domain

// CodeAlphabet omits visually ambiguous characters (0/O, 1/I/L).
const CodeAlphabet = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"
const CodeLength = 6

type RandomCodes struct{}

func (RandomCodes) Generate() string {
    b := make([]byte, CodeLength)
    rand.Read(b)
    for i := range b {
        b[i] = CodeAlphabet[int(b[i])%len(CodeAlphabet)]
    }
    return string(b)
}

Structs + methods instead of classes

Data and behaviour are declared separately. Constructors are plain functions.

type SessionID string // named type: can't pass a QuestionID by mistake

type Session struct {
    ID           SessionID
    State        SessionState
    CurrentIndex int // -1 in lobby
    Participants map[ParticipantID]*Participant
}

// NewSession creates a session in the lobby state.
func NewSession(id SessionID, now time.Time) *Session {
    return &Session{ID: id, State: StateLobby, CurrentIndex: -1,
        Participants: map[ParticipantID]*Participant{}}
}

func (s *Session) HasAnswered(pid ParticipantID) bool { ... }

Receivers:

Receiver Meaning
(s *Session) Pointer: method can mutate, no copy
(s Session) Value: method works on a copy

Rule of thumb: if any method needs a pointer receiver, give them all pointer receivers.

Enums with iota

type SessionState int

const (
    StateLobby SessionState = iota
    StateQuestionOpen
    StateQuestionClosed
    StateFinished
)

func (s SessionState) String() string { ... } // satisfies fmt.Stringer

Interfaces are implicit

A type satisfies an interface by having the methods. No implements.

// package service: the consumer declares what it needs
type Broadcaster interface {
    Broadcast(sessionID SessionID, ev Event)
}

// package httpapi: the real WebSocket hub, never mentions Broadcaster
func (h *Hub) Broadcast(id SessionID, ev Event) { ... }

// package service_test: a fake that just remembers
func (r *recorder) Broadcast(_ SessionID, ev Event) {
    r.events = append(r.events, ev)
}

Guidelines:

Optional compile-time check that a type satisfies an interface:

var _ Broadcaster = (*Hub)(nil)

Embedding instead of inheritance

type BaseRepo struct{ db *sql.DB }

func (r *BaseRepo) Save(o Order) error { return insert(r.db, o) }
func (r *BaseRepo) Audit(m string)     { log.Println(m) }

type OrderRepo struct {
    *BaseRepo // embedded: Save and Audit are promoted
}

// "override": same method name on the outer type
func (r *OrderRepo) Save(o Order) error {
    if err := validate(o); err != nil {
        return err
    }
    if err := r.BaseRepo.Save(o); err != nil { // "super"
        return err
    }
    r.Audit("saved " + o.ID) // promoted from BaseRepo
    return nil
}

The trap: no virtual dispatch through embedding

type Animal struct{}
func (Animal) Name() string       { return "animal" }
func (a Animal) Describe() string { return "I am a " + a.Name() }

type Dog struct{ Animal }
func (Dog) Name() string { return "dog" }

Dog{}.Describe() // "I am a animal" (!)

The embedded type never calls back into the outer type, so template-method designs silently break. The idiomatic replacement is a function that takes an interface: shared logic lives in one place, each implementation supplies only what differs.

// Shared behaviour: a test suite every store implementation must pass.
func RunStoreTests(t *testing.T, newStore func(*testing.T) Store) { ... }

// memory_test.go
storetest.RunStoreTests(t, func(*testing.T) Store { return memory.New() })

Polymorphism: interfaces, type switches, generics

Runtime type checks via type assertion. Always use the two-value form; the one-value form panics on mismatch.

p, ok := ctx.Value(ctxParticipant).(Participant) // ok is false if the type does not match

switch v := x.(type) {
case string:
    ...
case fmt.Stringer:
    ...
}

Compile-time reuse via type parameters (Go 1.18+):

// clone round-trips a value through JSON so callers can never mutate
// what the store holds.
func clone[T any](v *T) (*T, error) {
    raw, err := json.Marshal(v)
    if err != nil {
        return nil, err
    }
    var out T
    if err := json.Unmarshal(raw, &out); err != nil {
        return nil, err
    }
    return &out, nil
}

Generics are for algorithms and containers (see slices, maps, cmp in the stdlib). Behaviour still goes through interfaces.


Packages

cmd/server/main.go     ← wiring only
internal/domain/       ← types + business rules, no I/O
internal/httpapi/      ← transport
internal/store/...     ← persistence implementations