Featured image

This series of articles looks at various patterns and how they apply to Go, starting with the GoF creational patterns.

Verdicts: Link to heading

  • Idiomatic - Considered a best practice in Go.
  • Code Smell - Not wrong, but if you are using it, make sure you understand what you are doing.
  • Anti-Pattern - Avoid unless you are an expert and know it is okay to break this rule.

Singleton Link to heading

The singleton, while an anti-pattern, like most anti-patterns, has specific use cases. One industry that makes use of singletons is game development.

Problem Space Link to heading

Let’s say you are working with Ebitengine and want to create a player object for your game. This is one situation where this pattern may come in handy.

It goes beyond something like sync.Once which guarantees an object is only created once, but does not prevent clones.

Practical Example Link to heading

internal/player/player.go

package player

import (
	"sync"
)

var once sync.Once

type entity struct {
	health int
	//This makes it thread safe and also will give a warning if someone tries to clone the struct which Go does not prevent. This will only give a warning with go vet and not any runtime errors.
	mu sync.Mutex
}

var instance *entity

func GetPlayer() *entity {
	once.Do(func() {
		instance = &entity{}
	})
	return instance
}

func (e *entity) UpdatePlayerHealth(h int) int {
	e.mu.Lock()
	defer e.mu.Unlock()
	e.health += h
	return e.health
}

Considerations Link to heading

I would argue that even this is an anti-pattern. The approach I would take instead is…

cmd/main.go

package main

type game struct {
	player player.Player
}

func main() {
	g := game {
		player: player.New(),
	}
}

This allows you to have a standard package layout, since you only initialize it in main.go you get the protection of a singleton. Since you pass it in as part of the game object, you avoid messy global state.

You notice that the non-singleton version uses an exported Player type from player since we no longer need to hide the Player struct.

Verdict: Link to heading

Anti-Pattern

Builder Link to heading

Problem Space Link to heading

The builder pattern allows the creation of complex objects in a less error-prone manner.

When constructing objects with many optional parameters, or when construction requires multiple steps, passing everything to a constructor becomes unwieldy and error-prone.

Practical Example 1 Link to heading

internal/server/server.go

package server

import "time"

type Server struct {
    host     string
    port     int
    timeout  time.Duration
    maxConns int
}

type ServerBuilder struct {
    host     string
    port     int
    timeout  time.Duration
    maxConns int
}

func NewServerBuilder() *ServerBuilder {
    return &ServerBuilder{
        port:    8080,
        timeout: 30 * time.Second,
    }
}

func (b *ServerBuilder) SetHost(h string) {
    b.host = h
}

func (b *ServerBuilder) SetPort(p int) {
    b.port = p
}

func (b *ServerBuilder) SetTimeout(t time.Duration) {
    b.timeout = t
}

func (b *ServerBuilder) Build() *Server {
    return &Server{
        host:    b.host,
        port:    b.port,
        timeout: b.timeout,
    }
}

cmd/server/main.go

// Usage
builder := server.NewServerBuilder()
builder.SetHost("localhost")
builder.SetPort(9000)
builder.SetTimeout(60 * time.Second)
s := builder.Build()

Practical Example 2 Link to heading

internal/server/server.go

package server

import "time"

type Server struct {
    host     string
    port     int
    timeout  time.Duration
    maxConns int
}

type ServerBuilder struct {
    host     string
    port     int
    timeout  time.Duration
    maxConns int
}

func NewBuilder() *ServerBuilder {
    return &ServerBuilder{
        port:    8080,
        timeout: 30 * time.Second,
    }
}

func (b *ServerBuilder) Host(h string) *ServerBuilder {
    b.host = h
    return b
}

func (b *ServerBuilder) Port(p int) *ServerBuilder {
    b.port = p
    return b
}

func (b *ServerBuilder) Timeout(t time.Duration) *ServerBuilder {
    b.timeout = t
    return b
}

func (b *ServerBuilder) Build() *Server {
    return &Server{
        host:     b.host,
        port:     b.port,
        timeout:  b.timeout,
        maxConns: b.maxConns,
    }
}

cmd/server/main.go

// Usage
s := server.NewBuilder().
    Host("localhost").
    Port(9000).
    Timeout(60 * time.Second).
    Build()

Practical Example 3 Link to heading

internal/server/server.go

package server

import "time"

type Server struct {
    host     string
    port     int
    timeout  time.Duration
    maxConns int
}

type Option func(*Server)

func WithPort(port int) Option {
    return func(s *Server) {
        s.port = port
    }
}

func WithTimeout(t time.Duration) Option {
    return func(s *Server) {
        s.timeout = t
    }
}

func New(host string, opts ...Option) *Server {
    s := &Server{
        host:    host,
        port:    8080,
        timeout: 30 * time.Second,
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

cmd/server/main.go

// Usage
s := server.New("localhost",
    server.WithPort(9000),
    server.WithTimeout(60*time.Second),
)

Practical Example 4 Link to heading

internal/server/server.go

package server

import "time"

type Server struct {
    Host     string
    Port     int
    Timeout  time.Duration
    MaxConns int
}

func New(host string, port int) *Server {
    return &Server{
        Host:    host,
        Port:    port,
        Timeout: 30 * time.Second,
        MaxConns: 100,
    }
}

cmd/server/main.go

// Usage
s := server.New("localhost", 8080)

// Or just a struct literal if all fields are exported
s := &server.Server{
    Host:    "localhost",
    Port:    8080,
    Timeout: 60 * time.Second,
}

Considerations Link to heading

Example 1: This is the original GoF example. However, it has much more ceremony than Go typically wants. I would argue the next two examples are better and that this is an anti-pattern.

Example 2: The method chaining pattern often replaces the builder pattern in Go when building is the actual work you need to do. It is usually used in query and request builders. Please note the example above shows a server builder to keep all examples consistent.

Example 3: The functional options pattern is the Go-native alternative for configuration and often replaces the builder entirely.

Example 4: I would argue that this is the most idiomatic. Do you even need the builder pattern when we can easily create complex structs for little cost using the New pattern?

When deciding between using new and functional options, I almost always choose to use new. The only time I use functional options is when there are optional pre-configured parameters I do not want to override.

It is worth noting that in examples 1-3 you are allowed to keep the fields unexported, so you are encapsulating your data better, while example 4 does force you to expose your data to external manipulation.

Verdict: Link to heading

  • Example 1: Anti-Pattern
  • Example 2: Idiomatic for specific use cases.
  • Example 3: Idiomatic
  • Example 4: Idiomatic

Factory Pattern Link to heading

Problem Space Link to heading

When you need to create objects that share a common interface but have different implementations, or when the creation logic needs to be decoupled from the usage.

Practical Example 1 Link to heading

package storage

import "errors"

type Storage interface {
    Save(key string, data []byte) error
    Load(key string) ([]byte, error)
}

type memory struct {
    data map[string][]byte
}

func (m *memory) Save(key string, data []byte) error {
    m.data[key] = data
    return nil
}

func (m *memory) Load(key string) ([]byte, error) {
    d, ok := m.data[key]
    if !ok {
        return nil, errors.New("not found")
    }
    return d, nil
}

func NewMemory() Storage {
    return &memory{data: make(map[string][]byte)}
}

type disk struct {
    basePath string
}

func (d *disk) Save(key string, data []byte) error { return nil }
func (d *disk) Load(key string) ([]byte, error)    { return nil, nil }

func NewDisk(basePath string) Storage {
    return &disk{basePath: basePath}
}

Practical Example 2 Link to heading

package main

import (
	"errors"
	"fmt"
	"log"
)

// --- 1. The Interface ---
// We define the behavior we need. This is often defined by the CONSUMER,
// but for a library/factory package, it's common to export it here.
type Storage interface {
	Save(key string, value string) error
	Get(key string) (string, error)
}

// --- 2. Concrete Implementation: Memory ---

type MemoryStore struct {
	data map[string]string
}

// IDIOMATIC: This constructor returns the CONCRETE type (*MemoryStore).
// If a user just wants a memory map, they get the full struct.
func NewMemoryStore() *MemoryStore {
	return &MemoryStore{
		data: make(map[string]string),
	}
}

func (m *MemoryStore) Save(key, value string) error {
	m.data[key] = value
	fmt.Printf("[Memory] Saved %s to RAM\n", key)
	return nil
}

func (m *MemoryStore) Get(key string) (string, error) {
	val, ok := m.data[key]
	if !ok {
		return "", errors.New("not found")
	}
	return val, nil
}

// --- 3. Concrete Implementation: Disk ---

type DiskStore struct {
	path string
}

// IDIOMATIC: This constructor returns the CONCRETE type (*DiskStore).
func NewDiskStore(path string) *DiskStore {
	return &DiskStore{
		path: path,
	}
}

func (d *DiskStore) Save(key, value string) error {
	// (Simulating disk I/O)
	fmt.Printf("[Disk] Writing %s to file at %s\n", key, d.path)
	return nil
}

func (d *DiskStore) Get(key string) (string, error) {
	return "some data from disk", nil
}

// --- 4. The True Factory ---

type Config struct {
	Type     string // "memory" or "disk"
	FilePath string
}

// THIS is the Factory.
// It is the ONLY place strictly returning the interface is idiomatic here,
// because the return type is polymorphic based on runtime config.
func NewStorage(cfg Config) (Storage, error) {
	switch cfg.Type {
	case "memory":
		// We call the concrete constructor
		return NewMemoryStore(), nil
	case "disk":
		if cfg.FilePath == "" {
			return nil, errors.New("file path required for disk storage")
		}
		// We call the concrete constructor
		return NewDiskStore(cfg.FilePath), nil
	default:
		return nil, fmt.Errorf("unknown storage type: %s", cfg.Type)
	}
}

// --- 5. Usage ---

func main() {
	// Scenario A: The user knows they want Memory (e.g., in a unit test).
	// They use the concrete constructor directly. No interfaces forced on them.
	mem := NewMemoryStore()
	mem.Save("user_1", "direct_access")

	// Scenario B: The application is starting up based on ENV variables.
	// We don't know what we'll get, so we use the Factory.
	appConfig := Config{
		Type:     "disk",
		FilePath: "/var/data/db.json",
	}

	// db is of type 'Storage' interface
	db, err := NewStorage(appConfig)
	if err != nil {
		log.Fatal(err)
	}

	// The application code doesn't care which one it is
	db.Save("user_2", "factory_created")
}

Considerations Link to heading

Example 1: If this prickled your Spidey senses, you are not alone. The code smell coming from this is that it violates one of the idioms of Go, “accept interfaces and return structs”.

Example 2: This can be idiomatic if needed. However, I would argue that if you ever feel you need to do this, then you should look at your decision making up to that point to be sure this is the best way.

While there are some valid use cases for this pattern in Go, I would view it as a code smell at best, and an anti-pattern at worst.

Verdict Link to heading

  • Example 1: Anti-Pattern
  • Example 2: Code Smell

Abstract Factory Pattern Link to heading

Problem Space Link to heading

When you need to create groups of related objects that must be used together, and you want to ensure consistency across those objects. The classic example is UI toolkits — if you’re creating Windows-style widgets, all your buttons, checkboxes, and dialogs should be Windows-style, not a mix.

Practical Example 1 Link to heading

internal/ui/factory.go

package ui

type Button interface {
    Render() string
}

type Checkbox interface {
    Render() string
}

type GUIFactory interface {
    CreateButton() Button
    CreateCheckbox() Checkbox
}

// Windows implementation
type WindowsButton struct{}
func (b *WindowsButton) Render() string { return "Windows Button" }

type WindowsCheckbox struct{}
func (c *WindowsCheckbox) Render() string { return "Windows Checkbox" }

type WindowsFactory struct{}
func (f *WindowsFactory) CreateButton() Button { return &WindowsButton{} }
func (f *WindowsFactory) CreateCheckbox() Checkbox { return &WindowsCheckbox{} }

// Mac implementation
type MacButton struct{}
func (b *MacButton) Render() string { return "Mac Button" }

type MacCheckbox struct{}
func (c *MacCheckbox) Render() string { return "Mac Checkbox" }

type MacFactory struct{}
func (f *MacFactory) CreateButton() Button { return &MacButton{} }
func (f *MacFactory) CreateCheckbox() Checkbox { return &MacCheckbox{} }

func GetFactory(os string) GUIFactory {
    switch os {
    case "windows":
        return &WindowsFactory{}
    case "mac":
        return &MacFactory{}
    default:
        return &WindowsFactory{}
    }
}

cmd/main.go

package main

import "myapp/internal/ui"

func main() {
    factory := ui.GetFactory("mac")
    button := factory.CreateButton()
    checkbox := factory.CreateCheckbox()
    
    fmt.Println(button.Render())
    fmt.Println(checkbox.Render())
}

Practical Example 2 Link to heading

internal/ui/ui.go

package ui

type Button interface {
    Render() string
}

type Checkbox interface {
    Render() string
}

type Theme struct {
    NewButton   func() Button
    NewCheckbox func() Checkbox
}

var Windows = Theme{
    NewButton:   func() Button { return &windowsButton{} },
    NewCheckbox: func() Checkbox { return &windowsCheckbox{} },
}

var Mac = Theme{
    NewButton:   func() Button { return &macButton{} },
    NewCheckbox: func() Checkbox { return &macCheckbox{} },
}

type windowsButton struct{}
func (b *windowsButton) Render() string { return "Windows Button" }

type windowsCheckbox struct{}
func (c *windowsCheckbox) Render() string { return "Windows Checkbox" }

type macButton struct{}
func (b *macButton) Render() string { return "Mac Button" }

type macCheckbox struct{}
func (c *macCheckbox) Render() string { return "Mac Checkbox" }

cmd/main.go

package main

import "myapp/internal/ui"

func main() {
    theme := ui.Mac
    
    // This looks like a method call:
    button := theme.NewButton()
    checkbox := theme.NewCheckbox()
    
    // But it's actually:
	// 1. Access the field `NewButton` (which holds a function)
	// 2. Invoke that function with ()
    
    fmt.Println(button.Render())
    fmt.Println(checkbox.Render())
}

Considerations Link to heading

Example 1: The GoF version with a factory interface creating other interfaces. This is verbose and requires a lot of ceremony for what it accomplishes. The factory interface itself rarely adds value in Go.

Example 2: Replaces the factory interface with a struct containing constructor functions. More flexible — you can mix and match or modify at runtime. Feels more Go-native but still might be overkill.

The core question with Abstract Factory in Go is: do you actually need runtime swapping of entire object families? Usually the answer is no. You know at compile time (or at least at startup) which “family” you need, so a simple constructor handles it.

Verdict Link to heading

  • Example 1: Anti-Pattern
  • Example 2: Code Smell (use only if you genuinely need runtime family swapping, which is rare)

Prototype Pattern Link to heading

Problem Space Link to heading

When creating new objects by copying existing ones is more efficient or convenient than creating from scratch, especially when objects have complex initialization or you want to avoid repeated setup.

Practical Example 1 - GoF Method Link to heading

internal/document/document.go

package document

import (
    "maps"
    "slices"
)

type Document struct {
    Title    string
    Content  string
    Author   string
    Tags     []string
    Metadata map[string]string
}

func (d *Document) Copy() *Document {
	if d == nil { return nil }
    return &Document{
        Title:    d.Title,
        Content:  d.Content,
        Author:   d.Author,
        Tags:     slices.Clone(d.Tags),
        Metadata: maps.Clone(d.Metadata),
    }
}

cmd/main.go

package main

import "myapp/internal/document"

func main() {
    original := &document.Document{
        Title:    "Report",
        Content:  "...",
        Author:   "Steve",
        Tags:     []string{"draft", "q4"},
        Metadata: map[string]string{"version": "1"},
    }
    copy := original.Copy()
}

Practical Example 2 Link to heading

internal/document/document.go

package document

type Document struct {
    Title   string
    Content string
    Author  string
}

cmd/main.go

package main

import "myapp/internal/document"

func main() {
    original := document.Document{Title: "Report", Content: "...", Author: "Steve"}
    copy := original // Value semantics handles it
}

Considerations Link to heading

Example 1: Nearly a perfect match to the GoF pattern. When you have slices, maps, or pointers, you need explicit deep copy logic. A Copy() method returning the concrete type is cleaner than an interface-based Clone().

Example 2: For structs with only value types, Go’s value semantics handle copying automatically. No pattern needed.

Verdict Link to heading

  • Example 1: Idiomatic (for types with pointer semantics; i.e. slices, maps, and pointers)
  • Example 2: Idiomatic (for value types)