The Singleton pattern is a creational design pattern that ensures a class has only one instance and provides a global point of access to that instance. While Go does not have traditional classes like object-oriented languages, it does allow for the implementation of the Singleton pattern using various approaches. We are going to use Goroutines in testing our approaches in Multi-threading. In this article, we’ll explore different approaches to implement the Singleton pattern in Go, along with code examples.

Approach 1: Using Sync.Once

Go’s sync.Once package provides a thread-safe way to initialize a value exactly once. This makes it ideal for implementing a Singleton pattern without the need for explicit locking mechanisms.

package main

import (
    "fmt"
    "sync"
)

type singleton struct {
    // Add any necessary fields here
}

var once sync.Once
var instance *singleton

func getInstance() *singleton {
    once.Do(func() {
        instance = &singleton{}
        fmt.Println("Instance created")
    })
    return instance
}

func main() {
    fmt.Println("Main method started")
    for i := 0; i < 10; i++ {
        go fmt.Println("Instance at", i, " = ", getInstance())
    }
    fmt.Scanln()
}

Output:

$ go run singleton.go
Main method started
Instance created
Instance at 3  =  &{}
Instance at 2  =  &{}
Instance at 9  =  &{}
Instance at 7  =  &{}
Instance at 8  =  &{}
Instance at 4  =  &{}
Instance at 5  =  &{}
Instance at 0  =  &{}
Instance at 1  =  &{}
Instance at 6  =  &{}

Approach 2: Using init Function

In this approach, we leverage Go’s init function to initialize the singleton instance. The init function is called automatically when the package is initialized, ensuring that the instance is created before any other code execution.

package main

import (
    "fmt"
)

type singleton struct {
    // Add any necessary fields here
}

var instance *singleton

func init() {
    fmt.Println("Instance created")
    instance = &singleton{}
}

func getInstance() *singleton {
    return instance
}

func main() {
    fmt.Println("Main method started")
    for i := 0; i < 10; i++ {
        go fmt.Println("Instance at", i, " = ", getInstance())
    }
    fmt.Scanln()
}

Output:

$ go run singleton.go
Instance created
Main method started
Instance at 6  =  &{}
Instance at 1  =  &{}
Instance at 9  =  &{}
Instance at 7  =  &{}
Instance at 8  =  &{}
Instance at 5  =  &{}
Instance at 3  =  &{}
Instance at 0  =  &{}
Instance at 4  =  &{}
Instance at 2  =  &{}

Approach 3: Using synchronization with Mutex Locks

This approach ensures that only one instance of the single struct is created even in a concurrent environment. The use of mutex locks (sync.Mutex) provides thread safety by synchronizing access to the critical section where the instance is created. Once the instance is created, subsequent calls to getInstance() return the same instance without creating new ones.

package main

import (
    "fmt"
    "sync"
)

type singleton struct {
    // Add any necessary fields here
}

var lock = &sync.Mutex{}

var instance *singleton

func getInstance() *singleton {
    if instance == nil {
        // Only one goroutine at a time can go next
        lock.Lock()
        defer lock.Unlock()

        if instance == nil {
            fmt.Println("Instance created")
            instance = &singleton{}
        } else {
            fmt.Println("Instance exists")
        }
    } else {
        fmt.Println("Instance exists")
    }
    return instance
}

func main() {
    fmt.Println("Main method started")
    for i := 0; i < 10; i++ {
        go fmt.Println("Instance at", i, " = ", getInstance())
    }
    fmt.Scanln()
}

Output:

$ go run singleton.go
Main method started
Instance created
Instance exists
Instance exists
Instance exists
Instance exists
Instance exists
Instance exists
Instance exists
Instance exists
Instance exists
Instance at 3  =  &{}
Instance at 1  =  &{}
Instance at 6  =  &{}
Instance at 0  =  &{}
Instance at 4  =  &{}
Instance at 5  =  &{}
Instance at 8  =  &{}
Instance at 7  =  &{}
Instance at 9  =  &{}
Instance at 2  =  &{}

This approach is not very good considering performance and additional complexity.

Conclusion

In Go, implementing the Singleton pattern can be achieved using different approaches, each with its own advantages. Whether you prefer simplicity, thread safety, or initialization control, Go provides the flexibility to choose the most suitable approach for your specific requirements. Experiment with these approaches and choose the one that best fits your project needs.

Happy coding! 🚀