Menu
Coddy logo textTech

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 instance

Use 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 icon

Challenge

Easy

Let'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 Logger struct with two fields: Prefix (string) for log message prefixes, and MessageCount (int) to track how many messages have been logged.

    Use package-level variables with sync.Once to ensure only one instance is ever created. Implement a GetLogger() 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 prefix
    • Log(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 to GetLogger() 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 processed

Your output should be:

[APP] #1: Server started
[APP] #2: User connected
[APP] #3: Request processed
Total messages logged: 3

And given:

[DEBUG]
2
Initializing cache
Cache ready

Your output should be:

[DEBUG] #1: Initializing cache
[DEBUG] #2: Cache ready
Total messages logged: 2

And given:

[ERROR]
1
Connection failed

Your output should be:

[ERROR] #1: Connection failed
Total messages logged: 1

The 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 instance

Use 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"
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming