Design Patterns in Go: Not a "Religion," But a Tool
\n\nWhen I first started writing in Go, I was haunted by the feeling that all this "Gang of Four" stuff with their 23 patterns was something from the world of Java and C++. Go is so simple and concise — why do we need these complexities? But the deeper I delved into real projects (microservices, CLI utilities, API handlers), the more often I caught myself thinking: "A factory method would come in handy here" or "This mountain of if-else can be replaced with a strategy."
\n\nDesign patterns in Go are not a dogma or an attempt to force OOP onto a functional language. They are proven solutions to typical problems. They help make code testable, flexible, and understandable for colleagues. And yes, Go leaves its mark: due to the lack of classical inheritance and generics (before version 1.18), some patterns are transformed, while others become redundant.
\n\nIn this article, we won't list all 23 patterns. Instead, we'll break down the 5 most in-demand patterns in Golang that will actually be useful in production projects. You'll see when to apply them and when to run away without looking back.
\n\nThe Singleton Pattern in Go: Not Just a Singleton, But a Go-style Singleton
\n\nSingleton guarantees that a struct will have only one instance. In Go, this is often used for configs, connection pools, or loggers. But there's a nuance: Go doesn't have static classes, but it does have sync.Once — the perfect tool for lazy initialization.
The Problem Without Singleton
\nImagine you have a Config struct that reads settings from a file. If every package creates its own instance, you'll either read the file 10 times (slow) or get a race condition.
The Solution with Singleton
\npackage config\n\nimport (\n "sync"\n "encoding/json"\n "os"\n)\n\ntype Config struct {\n Port int `json:"port"`\n Env string `json:"env"`\n}\n\nvar (\n instance *Config\n once sync.Once\n)\n\nfunc GetConfig() *Config {\n once.Do(func() {\n file, _ := os.Open("config.json")\n defer file.Close()\n instance = &Config{}\n json.NewDecoder(file).Decode(instance)\n })\n return instance\n}\n\nWhen to use: when a guaranteed single instance is needed (configs, caches, factories).
\nWhen not to: for everything. A Singleton is global state that complicates testing. If you can pass a dependency through a constructor — do it.
\n\nThe Factory Pattern: When Object Creation Gets Complex
\n\nFactory Method and Abstract Factory are about encapsulating the logic of object creation. In Go, this is especially relevant since we don't have constructors in the classical sense. Instead, we use constructor functions with the New prefix.
Example: Creating Notifications
\nLet's say your system sends notifications via Email, SMS, and Push. Instead of spreading the selection logic throughout the code, we use a factory.
\n\npackage notification\n\nimport "fmt"\n\ntype Notifier interface {\n Send(message string) error\n}\n\ntype EmailNotifier struct{}\n\nfunc (e EmailNotifier) Send(message string) error {\n fmt.Println("Sending email:", message)\n return nil\n}\n\ntype SMSNotifier struct{}\n\nfunc (s SMSNotifier) Send(message string) error {\n fmt.Println("Sending SMS:", message)\n return nil\n}\n\nfunc NewNotifier(typ string) Notifier {\n switch typ {\n case "email":\n return EmailNotifier{}\n case "sms":\n return SMSNotifier{}\n default:\n return nil\n }\n}\n\nWhen to use: when the creation logic is complex (depends on type, version, config).
\nWhen not to: if you only have one type of object. Just write MyStruct{} or NewMyStruct() without a factory.