DEV Community

Divya Darshana
Divya Darshana

Posted on

Design Patterns in Golang: A Comprehensive Guide

Image description

Golang (or Go) has steadily gained popularity for its simplicity and efficiency, making it a favorite among developers. Understanding and implementing design patterns in Go can greatly enhance the scalability and maintainability of your applications. In this article, we will explore some of the most common design patterns in Golang, complete with code snippets and practical examples.


My Journey with Go and GoFr

As a final-year CSE undergraduate student, my journey with Golang began when I embarked on contributing to the GoFr—an open-source framework designed for building efficient web applications. It was an exciting challenge, as I explored a new language while diving into real-world development and best practices.

GoFr introduced me to several design patterns and best practices in Golang, which have shaped my approach to writing clean and scalable code. In this article, I’m excited to share these insights with you, as they’ve significantly improved my skills as a developer.


1. Factory (A Creational Design Pattern)

The Factory pattern is used to create objects without exposing the instantiation logic to the client. It provides a method that allows creating objects dynamically based on input parameters.

This is especially useful when you need to create objects that share the same interface or parent type but have different underlying implementations.

Example:

package main

import "fmt"

// Product interface
type Product interface {
    GetName() string
}

// ConcreteProductA
type ConcreteProductA struct{}

func (p *ConcreteProductA) GetName() string {
    return "Product A"
}

// ConcreteProductB
type ConcreteProductB struct{}

func (p *ConcreteProductB) GetName() string {
    return "Product B"
}

// Factory function
func CreateProduct(productType string) Product {
    switch productType {
    case "A":
        return &ConcreteProductA{}
    case "B":
        return &ConcreteProductB{}
    default:
        return nil
    }
}

func main() {
    // Using the factory to create products
    productA := CreateProduct("A")
    productB := CreateProduct("B")

    fmt.Println(productA.GetName()) // Output: Product A
    fmt.Println(productB.GetName()) // Output: Product B
}
Enter fullscreen mode Exit fullscreen mode

2. Singleton (A Creational Design Pattern)

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. This is useful for managing shared resources like configurations or database connections.

Example:

package main

import (
    "fmt"
    "sync"
)

type Singleton struct {}

var (
    instance *Singleton
    once     sync.Once
)

func GetInstance() *Singleton {
    once.Do(func() {
        instance = &Singleton{}
    })
    return instance
}

func main() {
    obj1 := GetInstance()
    obj2 := GetInstance()
    fmt.Println(obj1 == obj2) // true
}
Enter fullscreen mode Exit fullscreen mode

3. Adapter (A Structural Design Pattern)

The Adapter pattern acts as a bridge between two incompatible interfaces. This pattern allows you to use an existing class with a different interface.

Example:

package main

import "fmt"

type LegacyPrinter struct {}

func (l *LegacyPrinter) Print(s string) {
    fmt.Println("Legacy printer output:", s)
}

type ModernPrinter interface {
    PrintMessage(s string)
}

type PrinterAdapter struct {
    legacyPrinter *LegacyPrinter
}

func (p *PrinterAdapter) PrintMessage(s string) {
    p.legacyPrinter.Print(s)
}

func main() {
    legacy := &LegacyPrinter{}
    adapter := &PrinterAdapter{legacyPrinter: legacy}
    adapter.PrintMessage("Hello from adapter!")
}
Enter fullscreen mode Exit fullscreen mode

4. Observer Pattern (A Behavioral Design Pattern)

The Observer pattern defines a dependency between objects so that when one object changes state, all its dependents are notified.

Example:

package main

import "fmt"

type Observer interface {
    Update(string)
}

type Subject struct {
    observers []Observer
}

func (s *Subject) Attach(o Observer) {
    s.observers = append(s.observers, o)
}

func (s *Subject) Notify(msg string) {
    for _, o := range s.observers {
        o.Update(msg)
    }
}

type ConcreteObserver struct {
    name string
}

func (c *ConcreteObserver) Update(msg string) {
    fmt.Printf("%s received message: %s\n", c.name, msg)
}

func main() {
    subject := &Subject{}
    observer1 := &ConcreteObserver{name: "Observer1"}
    observer2 := &ConcreteObserver{name: "Observer2"}

    subject.Attach(observer1)
    subject.Attach(observer2)

    subject.Notify("Hello, Observers!")
}
Enter fullscreen mode Exit fullscreen mode

Options Pattern

The Options pattern is a flexible way to configure structs in Go, providing cleaner and more maintainable code. There are two common approaches:

1. Functional Options

Functional options use functions to modify the properties of a struct.

Example:

package main

import "fmt"

type Server struct {
    Host string
    Port int
}

func NewServer(opts ...func(*Server)) *Server {
    server := &Server{
        Host: "localhost",
        Port: 8080,
    }

    for _, opt := range opts {
        opt(server)
    }

    return server
}

func WithHost(host string) func(*Server) {
    return func(s *Server) {
        s.Host = host
    }
}

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

func main() {
    server := NewServer(WithHost("127.0.0.1"), WithPort(9090))
    fmt.Printf("Server: %+v\n", server)
}
Enter fullscreen mode Exit fullscreen mode

2. Builder Pattern for Options

The Builder pattern can also be used to configure structs with multiple optional parameters.

Example:

package main

import "fmt"

type Server struct {
    Host string
    Port int
}

type ServerBuilder struct {
    server Server
}

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

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

func (b *ServerBuilder) Build() Server {
    return b.server
}

func main() {
    builder := &ServerBuilder{}
    server := builder.SetHost("127.0.0.1").SetPort(9090).Build()
    fmt.Printf("Server: %+v\n", server)
}
Enter fullscreen mode Exit fullscreen mode

Mastering Design Patterns

A great way to get better at design patterns is through practical application. Weekend projects and contributing to open-source projects can accelerate your learning. One such project you can contribute to is GoFr, where I had the opportunity to enhance my Golang skills by working on real-world challenges.

You can also star the GoFr repository on GitHub to support the project and help it gain more visibility among developers. By doing so, you'll make it one of your favorites, making it easier to revisit and track updates.

Image description

Suggested Projects

  • GoFr: GitHub Repository
  • Build a simple REST API with different patterns.
  • Create a library implementing multiple design patterns in Go.

By working on these projects, you’ll gain hands-on experience and a deeper understanding of how design patterns solve real-world problems.

Top comments (1)

Collapse
 
sazardev profile image
Sazardev

Very useful patterns! Thank you.