Singleton Pattern
Part of the Object Oriented Programming section of Coddy's GO journey — lesson 86 of 107.
The Singleton pattern ensures that a type has only one instance throughout your program and provides a global point of access to it. This is useful for shared resources like configuration managers, database connections, or loggers.
In Go, we implement Singleton using a package-level variable combined with sync.Once to guarantee thread-safe initialization:
package config
import "sync"
type Config struct {
DatabaseURL string
MaxRetries int
}
var (
instance *Config
once sync.Once
)
func GetInstance() *Config {
once.Do(func() {
instance = &Config{
DatabaseURL: "localhost:5432",
MaxRetries: 3,
}
})
return instance
}The sync.Once guarantees that the initialization function runs exactly once, even when multiple goroutines call GetInstance() simultaneously. Every subsequent call returns the same instance without re-executing the initialization code.
cfg1 := config.GetInstance()
cfg2 := config.GetInstance()
// cfg1 and cfg2 point to the same instanceUse Singleton sparingly. While convenient, it introduces global state that can make testing harder and hide dependencies. Consider whether dependency injection might be a better alternative for your use case.
Challenge
EasyLet's build a Logger singleton that ensures only one logger instance exists throughout your application! This is a classic use case for the Singleton pattern—you want all parts of your program to share the same logger configuration and state.
You'll organize your code across two files:
logger.go: Implement your thread-safe Singleton logger.Create a
Loggerstruct with two fields:Prefix(string) for log message prefixes, andMessageCount(int) to track how many messages have been logged.Use package-level variables with
sync.Onceto ensure only one instance is ever created. Implement aGetLogger()function that initializes the logger with a default prefix of"[LOG]"and returns the singleton instance.Add these methods to your Logger:
SetPrefix(prefix string)- changes the logger's prefixLog(message string) string- increments the message count and returns a formatted string:[prefix] #[count]: [message]GetCount() int- returns the total number of messages logged
main.go: Demonstrate that multiple calls toGetLogger()return the same instance.Read a new prefix value, then read a count of messages followed by each message to log.
Get the logger instance, set the custom prefix, then log each message and print the result. After logging all messages, get the logger instance again (to prove it's the same one) and print the total message count.
The following inputs will be provided:
- Line 1: Custom prefix to set
- Line 2: Number of messages
- Following lines: Each message to log
For example, given:
[APP]
3
Server started
User connected
Request processedYour output should be:
[APP] #1: Server started
[APP] #2: User connected
[APP] #3: Request processed
Total messages logged: 3And given:
[DEBUG]
2
Initializing cache
Cache readyYour output should be:
[DEBUG] #1: Initializing cache
[DEBUG] #2: Cache ready
Total messages logged: 2And given:
[ERROR]
1
Connection failedYour output should be:
[ERROR] #1: Connection failed
Total messages logged: 1The key insight is that no matter how many times you call GetLogger(), you always get the same instance with its shared state. The message count persists because there's only one Logger in existence!
Cheat sheet
The Singleton pattern ensures a type has only one instance throughout your program and provides a global point of access to it. This is useful for shared resources like configuration managers, database connections, or loggers.
Implement Singleton using a package-level variable combined with sync.Once for thread-safe initialization:
package config
import "sync"
type Config struct {
DatabaseURL string
MaxRetries int
}
var (
instance *Config
once sync.Once
)
func GetInstance() *Config {
once.Do(func() {
instance = &Config{
DatabaseURL: "localhost:5432",
MaxRetries: 3,
}
})
return instance
}The sync.Once guarantees the initialization function runs exactly once, even when multiple goroutines call GetInstance() simultaneously. Every subsequent call returns the same instance:
cfg1 := config.GetInstance()
cfg2 := config.GetInstance()
// cfg1 and cfg2 point to the same instanceUse Singleton sparingly as it introduces global state that can make testing harder and hide dependencies. Consider dependency injection as an alternative.
Try it yourself
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Read the custom prefix
prefix, _ := reader.ReadString('\n')
prefix = strings.TrimSpace(prefix)
// Read the number of messages
countStr, _ := reader.ReadString('\n')
countStr = strings.TrimSpace(countStr)
count, _ := strconv.Atoi(countStr)
// Read each message
messages := make([]string, count)
for i := 0; i < count; i++ {
msg, _ := reader.ReadString('\n')
messages[i] = strings.TrimSpace(msg)
}
// TODO: Get the logger instance using GetLogger()
// TODO: Set the custom prefix using SetPrefix()
// TODO: Log each message and print the result
// TODO: Get the logger instance again (to prove it's the same one)
// and print the total message count in format: "Total messages logged: X"
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of Go OOP
External FilesGo Workspace & ModulesPackages & ImportsExported vs Unexported NamesIntroduction to OOP in GoStructs as ClassesDefining Methods on StructsPointer vs Value ReceiversStruct InitializationConstructor FunctionsRecap - Simple Calculator4Interfaces
Introduction to InterfacesImplicit ImplementationInterface as ContractEmpty Interface (any)Type AssertionType SwitchInterface CompositionStringer & Error InterfacesRecap - Shape Calculator7Encapsulation
Exported vs Unexported FieldsPackage-Level EncapsulationGetter & Setter MethodsInformation Hiding in GoRecap - Student Records10Generics (Go 1.18+)
Introduction to GenericsType ParametersType ConstraintsGeneric StructsGeneric Methods WorkaroundRecap - Generic Collection13Design Patterns Part 1
Intro to Design PatternsSingleton PatternFactory PatternAbstract Factory PatternObserver PatternStrategy Pattern2Types & Structs Deep Dive
Basic & Composite TypesCustom Type DefinitionsStruct TagsAnonymous StructsNested StructsZero Values & DefaultsRecap - Contact Book5Composition Over Inheritance
Why Go Has No InheritanceStruct Embedding BasicsMethod PromotionEmbedding Multiple StructsEmbedding vs AggregationShadowing Embedded MethodsRecap - Employee Hierarchy8Error Handling & OOP
The error InterfaceCustom Error TypesError Wrapping (fmt.Errorf)Sentinel Errorserrors.Is() and errors.As()Panic, Defer, and RecoverRecap - File Parser3Pointers & Memory
Pointer Basics in GoPointers to StructsPass by Value vs ReferenceThe new() FunctionGarbage Collection in GoRecap - Linked List Builder6Polymorphism in Go
Polymorphism via InterfacesDuck Typing in GoInterface Satisfaction RulesPolymorphic CollectionsDependency InjectionRecap - Payment Processor9Concurrency & OOP
Goroutines BasicsChannels & CommunicationBuffered vs Unbuffered ChanSelect Statementsync.Mutex & sync.RWMutexsync.WaitGroupThread-Safe Struct DesignRecap - Worker Pool