Design patterns are reusable ways to solve common software design problems. In Go, the best pattern is usually the one that keeps the code small, explicit, and easy to change.
This guide covers the 23 classic design patterns with Go examples. Each section explains the idea, when it helps, and the simplest version worth remembering.
1. Singleton
Singleton makes sure there is only one instance of something shared, such as app configuration, a logger, or a database pool.
Use it when one shared object is truly enough for the whole program. In Go, prefer package-level values or sync.Once instead of hiding global state behind clever APIs.
package config
import "sync"
type Config struct {
AppName string
Port int
}
var (
once sync.Once
cfg *Config
)
func Get() *Config {
once.Do(func() {
cfg = &Config{
AppName: "store-api",
Port: 8080,
}
})
return cfg
}The important part is sync.Once: it keeps initialization safe even when multiple goroutines call Get at the same time.
2. Factory Method
Factory Method creates values without forcing the caller to know the concrete type.
Use it when the caller knows what it wants by name, type, or configuration, but should not construct the implementation directly.
type Notifier interface {
Send(to string, message string) error
}
type EmailNotifier struct{}
func (EmailNotifier) Send(to string, message string) error {
return nil
}
type SMSNotifier struct{}
func (SMSNotifier) Send(to string, message string) error {
return nil
}
func NewNotifier(channel string) (Notifier, error) {
switch channel {
case "email":
return EmailNotifier{}, nil
case "sms":
return SMSNotifier{}, nil
default:
return nil, fmt.Errorf("unknown channel: %s", channel)
}
}The caller depends on Notifier, not on EmailNotifier or SMSNotifier. That keeps switching logic in one place.
3. Abstract Factory
Abstract Factory creates related objects that must be used together.
Use it when your system has families of implementations, such as cloud providers, UI themes, payment providers, or storage backends.
type BlobStore interface {
Put(key string, value []byte) error
}
type Queue interface {
Publish(topic string, body []byte) error
}
type CloudFactory interface {
BlobStore() BlobStore
Queue() Queue
}
type AWSFactory struct{}
func (AWSFactory) BlobStore() BlobStore {
return S3Store{}
}
func (AWSFactory) Queue() Queue {
return SQSQueue{}
}The factory prevents accidental mixing, such as using one provider for storage and another for queues when the app expects a single environment.
4. Builder
Builder creates complex objects step by step while keeping construction readable.
Use it when a value has many optional fields, validation rules, or defaults that would make a constructor hard to read.
type Server struct {
Host string
Port int
TLS bool
RateLimit int
}
type ServerBuilder struct {
server Server
}
func NewServerBuilder() *ServerBuilder {
return &ServerBuilder{
server: Server{
Host: "localhost",
Port: 8080,
RateLimit: 100,
},
}
}
func (b *ServerBuilder) WithHost(host string) *ServerBuilder {
b.server.Host = host
return b
}
func (b *ServerBuilder) WithTLS(enabled bool) *ServerBuilder {
b.server.TLS = enabled
return b
}
func (b *ServerBuilder) Build() Server {
return b.server
}Builder works best when it improves readability. If a struct literal is already clear, use the struct literal.
5. Prototype
Prototype creates a new object by copying an existing one.
Use it when making a fresh object is expensive or when most new values start from the same base configuration.
type Report struct {
Title string
Filters map[string]string
}
func (r Report) Clone() Report {
filters := make(map[string]string, len(r.Filters))
for key, value := range r.Filters {
filters[key] = value
}
return Report{
Title: r.Title,
Filters: filters,
}
}The key detail is the deep copy of the map. A shallow copy would make the original and clone share mutable state.
6. Adapter
Adapter lets one interface work like another.
Use it when you need to use old code, a third-party package, or an external service without spreading its API across your app.
type PaymentGateway interface {
Charge(amount int) error
}
type StripeClient struct{}
func (StripeClient) CreatePayment(cents int, currency string) error {
return nil
}
type StripeAdapter struct {
client StripeClient
}
func (a StripeAdapter) Charge(amount int) error {
return a.client.CreatePayment(amount, "USD")
}The app speaks in its own interface, PaymentGateway. The adapter handles the external shape.
7. Bridge
Bridge separates what an object does from how that work is implemented.
Use it when you have two dimensions that change independently, such as notification type and delivery channel.
type Sender interface {
Send(message string) error
}
type EmailSender struct{}
func (EmailSender) Send(message string) error {
return nil
}
type Alert struct {
sender Sender
}
func NewAlert(sender Sender) Alert {
return Alert{sender: sender}
}
func (a Alert) Critical(message string) error {
return a.sender.Send("critical: " + message)
}Alert can change its message rules without changing email, SMS, or webhook senders.
8. Composite
Composite treats single objects and groups of objects through the same interface.
Use it for trees: folders and files, menus and menu items, teams and employees, categories and products.
type Node interface {
Size() int
}
type File struct {
bytes int
}
func (f File) Size() int {
return f.bytes
}
type Folder struct {
children []Node
}
func (f Folder) Size() int {
total := 0
for _, child := range f.children {
total += child.Size()
}
return total
}The caller can ask any Node for its size without knowing whether it is a file or a folder.
9. Decorator
Decorator adds behavior around an object without changing the object itself.
Use it for logging, metrics, tracing, caching, retries, compression, and authorization.
type Handler interface {
Handle(request string) string
}
type SearchHandler struct{}
func (SearchHandler) Handle(request string) string {
return "results for " + request
}
type LoggingHandler struct {
next Handler
}
func (h LoggingHandler) Handle(request string) string {
start := time.Now()
result := h.next.Handle(request)
fmt.Println(time.Since(start))
return result
}The decorated object still satisfies the same interface, so callers do not need to change.
10. Facade
Facade gives a simple API over a larger set of moving parts.
Use it when a workflow touches several services and you want the rest of the app to call one clean method.
type Inventory struct{}
func (Inventory) Reserve(productID string) error {
return nil
}
type Payment struct{}
func (Payment) Charge(userID string, cents int) error {
return nil
}
type Shipping struct{}
func (Shipping) Schedule(productID string) error {
return nil
}
type Checkout struct {
inventory Inventory
payment Payment
shipping Shipping
}
func (c Checkout) Buy(userID string, productID string, cents int) error {
if err := c.inventory.Reserve(productID); err != nil {
return err
}
if err := c.payment.Charge(userID, cents); err != nil {
return err
}
return c.shipping.Schedule(productID)
}The facade does not remove complexity. It puts the complexity in one obvious place.
11. Flyweight
Flyweight shares common data instead of duplicating it across many objects.
Use it when you create many small objects that repeat the same expensive data.
type Font struct {
Family string
Size int
}
type FontFactory struct {
cache map[string]*Font
}
func NewFontFactory() *FontFactory {
return &FontFactory{cache: map[string]*Font{}}
}
func (f *FontFactory) Get(family string, size int) *Font {
key := fmt.Sprintf("%s:%d", family, size)
if font, ok := f.cache[key]; ok {
return font
}
font := &Font{Family: family, Size: size}
f.cache[key] = font
return font
}Flyweight is useful only when sharing actually saves memory or setup cost. Do not add it for ordinary small structs.
12. Proxy
Proxy controls access to another object.
Use it for lazy loading, permission checks, rate limits, caching, or remote service calls.
type Image interface {
Display() error
}
type RealImage struct {
path string
}
func (i RealImage) Display() error {
return nil
}
type LazyImage struct {
path string
image *RealImage
}
func (i *LazyImage) Display() error {
if i.image == nil {
i.image = &RealImage{path: i.path}
}
return i.image.Display()
}The proxy has the same interface as the real object, but controls when the real object is created.
13. Observer
Observer notifies many subscribers when something changes.
Use it for events, cache invalidation, UI updates, audit logs, and background jobs triggered by domain changes.
type Event struct {
Name string
Data string
}
type Subscriber func(Event)
type EventBus struct {
subscribers []Subscriber
}
func (b *EventBus) Subscribe(subscriber Subscriber) {
b.subscribers = append(b.subscribers, subscriber)
}
func (b EventBus) Publish(event Event) {
for _, subscriber := range b.subscribers {
subscriber(event)
}
}This keeps the publisher simple. It does not need to know who reacts to the event.
14. Strategy
Strategy swaps an algorithm without changing the caller.
Use it when a choice like pricing, sorting, validation, ranking, or routing changes by context.
type DiscountStrategy interface {
Apply(cents int) int
}
type NoDiscount struct{}
func (NoDiscount) Apply(cents int) int {
return cents
}
type PercentDiscount struct {
Percent int
}
func (d PercentDiscount) Apply(cents int) int {
return cents - cents*d.Percent/100
}
type Cart struct {
discount DiscountStrategy
}
func (c Cart) Total(cents int) int {
return c.discount.Apply(cents)
}Strategy is often better than a large switch when the behavior grows independently.
15. Command
Command turns an action into a value.
Use it for queues, undo, retries, scheduled jobs, audit logs, and request pipelines.
type Command interface {
Execute() error
}
type EmailService struct{}
func (EmailService) Send(to string, body string) error {
return nil
}
type SendEmailCommand struct {
service EmailService
to string
body string
}
func (c SendEmailCommand) Execute() error {
return c.service.Send(c.to, c.body)
}
func Run(command Command) error {
return command.Execute()
}Once an action is a value, it can be stored, delayed, retried, or passed around.
16. State
State changes behavior when an object changes state.
Use it when an object has clear modes and each mode handles the same action differently.
type OrderState interface {
Pay(order *Order) error
Ship(order *Order) error
}
type Order struct {
state OrderState
}
func (o *Order) Pay() error {
return o.state.Pay(o)
}
func (o *Order) Ship() error {
return o.state.Ship(o)
}
type PendingState struct{}
func (PendingState) Pay(order *Order) error {
order.state = PaidState{}
return nil
}
func (PendingState) Ship(order *Order) error {
return errors.New("order is not paid")
}
type PaidState struct{}
func (PaidState) Pay(order *Order) error {
return errors.New("order is already paid")
}
func (PaidState) Ship(order *Order) error {
return nil
}State keeps mode-specific rules out of one giant conditional block.
17. Template Method
Template Method defines the steps of an algorithm while letting specific steps vary.
Go does not have inheritance-based templates like some object-oriented languages, so the clean version is usually a function that accepts an interface or callbacks.
type Importer interface {
Read() ([]byte, error)
Parse([]byte) ([]Record, error)
Save([]Record) error
}
type Record struct {
ID string
}
func RunImport(importer Importer) error {
data, err := importer.Read()
if err != nil {
return err
}
records, err := importer.Parse(data)
if err != nil {
return err
}
return importer.Save(records)
}The order of the algorithm stays fixed, while each importer decides how to read, parse, and save.
18. Chain of Responsibility
Chain of Responsibility passes a request through handlers until one handles it.
Use it for middleware, validation pipelines, support ticket routing, and request processing.
type Middleware func(http.Handler) http.Handler
func Chain(handler http.Handler, middleware ...Middleware) http.Handler {
for i := len(middleware) - 1; i >= 0; i-- {
handler = middleware[i](handler)
}
return handler
}
func Auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") == "" {
w.WriteHeader(http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}Go web middleware is one of the cleanest real-world examples of this pattern.
19. Mediator
Mediator coordinates communication between objects so they do not depend on each other directly.
Use it when many parts need to coordinate and direct references would create tangled dependencies.
type ChatRoom struct {
users map[string]User
}
type User struct {
Name string
}
func NewChatRoom() *ChatRoom {
return &ChatRoom{users: map[string]User{}}
}
func (r *ChatRoom) Join(user User) {
r.users[user.Name] = user
}
func (r ChatRoom) Send(from string, to string, message string) error {
if _, ok := r.users[from]; !ok {
return errors.New("sender not found")
}
if _, ok := r.users[to]; !ok {
return errors.New("receiver not found")
}
return nil
}The users do not talk to each other directly. The room owns the coordination rules.
20. Iterator
Iterator exposes a way to move through a collection without exposing how the collection is stored.
Use it when traversal needs its own logic, such as pagination, streaming, filtering, or reading from an external source.
type User struct {
ID string
Name string
}
type UserIterator struct {
users []User
index int
}
func (i *UserIterator) Next() (User, bool) {
if i.index >= len(i.users) {
return User{}, false
}
user := i.users[i.index]
i.index++
return user, true
}For ordinary slices, range is enough. Write an iterator when traversal has behavior worth naming.
21. Memento
Memento captures state so it can be restored later.
Use it for undo, drafts, checkpoints, form recovery, or rolling back local changes.
type Snapshot struct {
content string
}
type Editor struct {
content string
}
func (e Editor) Save() Snapshot {
return Snapshot{content: e.content}
}
func (e *Editor) Restore(snapshot Snapshot) {
e.content = snapshot.content
}
func (e *Editor) Write(content string) {
e.content = content
}The snapshot stores only what is needed to restore the editor, not every detail of the editor implementation.
22. Visitor
Visitor adds operations to a group of types without putting every operation on the types themselves.
Use it when you have a stable object structure but need to add new operations, such as rendering, exporting, validation, or evaluation.
type Shape interface {
Accept(visitor ShapeVisitor) float64
}
type ShapeVisitor interface {
VisitCircle(Circle) float64
VisitRectangle(Rectangle) float64
}
type Circle struct {
Radius float64
}
func (c Circle) Accept(visitor ShapeVisitor) float64 {
return visitor.VisitCircle(c)
}
type Rectangle struct {
Width float64
Height float64
}
func (r Rectangle) Accept(visitor ShapeVisitor) float64 {
return visitor.VisitRectangle(r)
}
type AreaVisitor struct{}
func (AreaVisitor) VisitCircle(circle Circle) float64 {
return math.Pi * circle.Radius * circle.Radius
}
func (AreaVisitor) VisitRectangle(rectangle Rectangle) float64 {
return rectangle.Width * rectangle.Height
}Visitor can feel heavy in Go. Use it only when the structure is stable and the operations keep growing.
23. Interpreter
Interpreter evaluates a small language or expression tree.
Use it for rules, filters, search queries, calculators, permissions, or simple domain-specific expressions.
type Expression interface {
Eval(input map[string]int) bool
}
type GreaterThan struct {
Name string
Value int
}
func (e GreaterThan) Eval(input map[string]int) bool {
return input[e.Name] > e.Value
}
type And struct {
Left Expression
Right Expression
}
func (e And) Eval(input map[string]int) bool {
return e.Left.Eval(input) && e.Right.Eval(input)
}This pattern is powerful when rules must be composed. If the language grows large, use a real parser instead of hand-building everything.
How to Choose the Right Pattern in Go
Start with plain functions, structs, and interfaces. Add a design pattern only when it makes the code easier to change.
Use creational patterns when object construction is noisy or needs rules: Singleton, Factory Method, Abstract Factory, Builder, Prototype.
Use structural patterns when types need to fit together cleanly: Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy.
Use behavioral patterns when control flow or responsibilities need to stay flexible: Observer, Strategy, Command, State, Template Method, Chain of Responsibility, Mediator, Iterator, Memento, Visitor, Interpreter.
The Go version of a design pattern should feel boring. Small interfaces, explicit dependencies, readable constructors, and simple composition usually beat clever architecture.