Skip to main content

Overview

This guide covers design patterns and architectural approaches found throughout the Learn Go course. These patterns are demonstrated in real projects like the tic-tac-toe game, log parser, and various other examples.

Structural Patterns

Composition Over Inheritance

Go doesn’t have inheritance, but uses composition through embedding.
From 24-structs/04-embedding
// Base components
type Address struct {
    Street string
    City   string
    Zip    string
}

type Contact struct {
    Email string
    Phone string
}

// Composed struct
type Person struct {
    Name    string
    Address // Embedded - promotes fields
    Contact // Embedded
}

// Usage - embedded fields are promoted
person := Person{
    Name: "Alice",
}
person.Street = "123 Main St"  // Direct access
person.Email = "alice@example.com"
Embedding promotes the embedded type’s fields and methods to the outer type, creating a composition relationship.

Interface Segregation

From interfaces examples
// Small, focused interfaces
type Printer interface {
    Print()
}

type Discounter interface {
    Discount() float64
}

// Types implement only what they need
type Book struct {
    title string
    price float64
}

func (b Book) Print() {
    fmt.Printf("%s: $%.2f\n", b.title, b.price)
}

type Game struct {
    title string
    price float64
}

func (g *Game) Print() {
    fmt.Printf("%s: $%.2f\n", g.title, g.price)
}

func (g *Game) Discount() float64 {
    return g.price * 0.5
}

Strategy Pattern

Inspired by x-tba/tictactoe/16-types
// Define the strategy interface
type Renderer interface {
    Render(board Board) string
}

// Concrete strategies
type ASCIIRenderer struct{}

func (r ASCIIRenderer) Render(board Board) string {
    // ASCII rendering logic
    return "X | O | X\n---------\n..."
}

type UnicodeRenderer struct{}

func (r UnicodeRenderer) Render(board Board) string {
    // Unicode rendering logic
    return "✗ │ ⭕ │ ✗\n─────────\n..."
}

// Context uses the strategy
type Game struct {
    board    Board
    renderer Renderer
}

func (g *Game) Display() {
    output := g.renderer.Render(g.board)
    fmt.Println(output)
}

// Usage
game := Game{
    board:    NewBoard(),
    renderer: UnicodeRenderer{},
}

Behavioral Patterns

Iterator Pattern (Using Range)

// Go's range provides built-in iteration
type Playlist struct {
    songs []string
}

func (p *Playlist) Add(song string) {
    p.songs = append(p.songs, song)
}

// Return slice for range iteration
func (p *Playlist) Songs() []string {
    return p.songs
}

// Usage
playlist := &Playlist{}
playlist.Add("Song 1")
playlist.Add("Song 2")

for i, song := range playlist.Songs() {
    fmt.Printf("%d: %s\n", i, song)
}

State Pattern

Inspired by x-tba/tictactoe examples
// Define states
type GameState int

const (
    StateInit GameState = iota
    StatePlaying
    StateWon
    StateDraw
    StateQuit
)

// Game with state
type Game struct {
    state  GameState
    board  Board
    turn   Player
}

// State-dependent behavior
func (g *Game) HandleInput(input string) error {
    switch g.state {
    case StateInit:
        return g.handleInit(input)
    case StatePlaying:
        return g.handlePlaying(input)
    case StateWon, StateDraw:
        return g.handleEnded(input)
    default:
        return errors.New("invalid state")
    }
}

func (g *Game) handlePlaying(input string) error {
    // Process move
    if err := g.board.Place(input, g.turn); err != nil {
        return err
    }
    
    // Check win condition
    if g.board.HasWinner() {
        g.state = StateWon
        return nil
    }
    
    if g.board.IsFull() {
        g.state = StateDraw
    }
    
    g.turn = g.turn.Next()
    return nil
}

Command Pattern

From project examples
// Command interface
type Command interface {
    Execute() error
}

// Concrete commands
type MoveCommand struct {
    board    *Board
    position int
    player   Player
}

func (c *MoveCommand) Execute() error {
    return c.board.PlaceAt(c.position, c.player)
}

type UndoCommand struct {
    board    *Board
    position int
}

func (c *UndoCommand) Execute() error {
    c.board.Clear(c.position)
    return nil
}

// Invoker
type GameController struct {
    history []Command
}

func (gc *GameController) Execute(cmd Command) error {
    if err := cmd.Execute(); err != nil {
        return err
    }
    gc.history = append(gc.history, cmd)
    return nil
}

Creational Patterns

Factory Function

From course examples
// Instead of constructors, use New functions
func NewGame(skin string) *Game {
    return &Game{
        board:  NewBoard(),
        skin:   parseSkin(skin),
        turn:   PlayerX,
        state:  StateInit,
    }
}

func NewBoard() Board {
    return Board{
        cells: make([]Cell, 9),
    }
}

// Usage
game := NewGame("unicode")
The New prefix is idiomatic in Go for constructor functions. Use NewType for the default constructor.

Builder Pattern

// Builder for complex objects
type PersonBuilder struct {
    person *Person
}

func NewPersonBuilder() *PersonBuilder {
    return &PersonBuilder{
        person: &Person{},
    }
}

func (b *PersonBuilder) Name(name string) *PersonBuilder {
    b.person.Name = name
    return b
}

func (b *PersonBuilder) Age(age int) *PersonBuilder {
    b.person.Age = age
    return b
}

func (b *PersonBuilder) Email(email string) *PersonBuilder {
    b.person.Email = email
    return b
}

func (b *PersonBuilder) Build() *Person {
    return b.person
}

// Usage - fluent interface
person := NewPersonBuilder().
    Name("Alice").
    Age(30).
    Email("alice@example.com").
    Build()

Singleton Pattern

// Package-level variable (lazy initialization)
var (
    instance *Database
    once     sync.Once
)

func GetDatabase() *Database {
    once.Do(func() {
        instance = &Database{
            // Initialize
        }
    })
    return instance
}

Functional Patterns

Option Pattern

// For configurable constructors
type GameOption func(*Game)

func WithSkin(skin string) GameOption {
    return func(g *Game) {
        g.skin = parseSkin(skin)
    }
}

func WithPlayers(p1, p2 string) GameOption {
    return func(g *Game) {
        g.player1 = p1
        g.player2 = p2
    }
}

func NewGame(options ...GameOption) *Game {
    g := &Game{
        board: NewBoard(),
        turn:  PlayerX,
    }
    
    for _, opt := range options {
        opt(g)
    }
    
    return g
}

// Usage
game := NewGame(
    WithSkin("unicode"),
    WithPlayers("Alice", "Bob"),
)
The Option pattern is idiomatic in Go for creating flexible APIs with optional configuration.

Pipeline Pattern

From log parser examples
// Pipeline stages
func ReadLines(filename string) <-chan string {
    out := make(chan string)
    go func() {
        defer close(out)
        // Read and send lines
    }()
    return out
}

func ParseLines(in <-chan string) <-chan LogEntry {
    out := make(chan LogEntry)
    go func() {
        defer close(out)
        for line := range in {
            if entry, err := Parse(line); err == nil {
                out <- entry
            }
        }
    }()
    return out
}

func FilterErrors(in <-chan LogEntry) <-chan LogEntry {
    out := make(chan LogEntry)
    go func() {
        defer close(out)
        for entry := range in {
            if entry.Level == "ERROR" {
                out <- entry
            }
        }
    }()
    return out
}

// Usage - compose pipeline
lines := ReadLines("app.log")
entries := ParseLines(lines)
errors := FilterErrors(entries)

for err := range errors {
    fmt.Println(err)
}

Data Access Patterns

Repository Pattern

// Define repository interface
type GameRepository interface {
    Save(game *Game) error
    Load(id string) (*Game, error)
    Delete(id string) error
}

// Concrete implementation
type FileRepository struct {
    basePath string
}

func NewFileRepository(path string) *FileRepository {
    return &FileRepository{basePath: path}
}

func (r *FileRepository) Save(game *Game) error {
    data, err := json.Marshal(game)
    if err != nil {
        return err
    }
    
    filename := filepath.Join(r.basePath, game.ID+".json")
    return os.WriteFile(filename, data, 0644)
}

func (r *FileRepository) Load(id string) (*Game, error) {
    filename := filepath.Join(r.basePath, id+".json")
    data, err := os.ReadFile(filename)
    if err != nil {
        return nil, err
    }
    
    var game Game
    if err := json.Unmarshal(data, &game); err != nil {
        return nil, err
    }
    
    return &game, nil
}

Error Handling Patterns

Sentinel Errors

// Define package-level errors
var (
    ErrInvalidMove = errors.New("invalid move")
    ErrGameOver    = errors.New("game is over")
    ErrOutOfBounds = errors.New("position out of bounds")
)

// Usage
func (g *Game) PlaceMove(pos int) error {
    if g.state == StateWon || g.state == StateDraw {
        return ErrGameOver
    }
    
    if pos < 0 || pos >= 9 {
        return ErrOutOfBounds
    }
    
    if !g.board.IsEmpty(pos) {
        return ErrInvalidMove
    }
    
    return g.board.Place(pos, g.turn)
}

// Checking specific errors
if err := game.PlaceMove(5); err != nil {
    if errors.Is(err, ErrInvalidMove) {
        fmt.Println("That position is taken!")
    }
}

Error Wrapping

From 11-if examples
func processFile(filename string) error {
    data, err := readFile(filename)
    if err != nil {
        return fmt.Errorf("failed to read %s: %w", filename, err)
    }
    
    if err := validate(data); err != nil {
        return fmt.Errorf("validation failed: %w", err)
    }
    
    return nil
}

// Unwrapping errors
err := processFile("data.txt")
if err != nil {
    if errors.Is(err, os.ErrNotExist) {
        fmt.Println("File doesn't exist")
    }
}

Testing Patterns

Table-Driven Tests

From tictactoe examples
func TestBoard_Place(t *testing.T) {
    tests := []struct {
        name     string
        position int
        player   Player
        wantErr  bool
    }{
        {
            name:     "valid move",
            position: 0,
            player:   PlayerX,
            wantErr:  false,
        },
        {
            name:     "out of bounds",
            position: 10,
            player:   PlayerX,
            wantErr:  true,
        },
        {
            name:     "position taken",
            position: 0,
            player:   PlayerO,
            wantErr:  true,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            board := NewBoard()
            err := board.Place(tt.position, tt.player)
            
            if (err != nil) != tt.wantErr {
                t.Errorf("Place() error = %v, wantErr %v", err, tt.wantErr)
            }
        })
    }
}
Table-driven tests are the idiomatic way to test in Go. They make it easy to add test cases and see patterns.

Test Helpers

func setupGame(t *testing.T) *Game {
    t.Helper() // Marks this as a helper function
    
    game := NewGame()
    // Setup logic
    return game
}

func TestGameLogic(t *testing.T) {
    game := setupGame(t)
    // Test with game
}

Best Practices

Go best practices guide

Go Idioms

Common Go idioms

Troubleshooting

Common issues and solutions

Examples

Browse code examples

Build docs developers (and LLMs) love