Design Pattern Series - Singleton Pattern (Golang)

Go Design Pattern Series 3 Th06 2025

Bài trước trong series: Abstract Factory Pattern

Singleton Pattern là gì?

Singleton Pattern là một Creational Pattern đảm bảo rằng một struct chỉ có duy nhất một instance trong suốt vòng đời ứng dụng, đồng thời cung cấp quyền truy cập toàn cục (global access) tới instance đó.

Trong bài viết này, chúng ta sẽ tìm hiểu lý do sử dụng Singleton Pattern, cách triển khai trong Go, các nhược điểm và giải pháp khắc phục.

Khi nào nên dùng Singleton Pattern trong Go?

Singleton Pattern thường được dùng khi bạn muốn:

  • Kiểm soát tài nguyên: Đảm bảo chỉ có duy nhất một instance của một tài nguyên.
  • Truy cập toàn cục: Cung cấp một điểm truy cập duy nhất đến tài nguyên chia sẻ.
  • Tối ưu hiệu năng: Tránh việc tạo ra nhiều instance nặng nề.

Ví dụ:

  • Quản lý kết nối cơ sở dữ liệu.
  • Quản lý configuration managers.
  • Logging services.
  • Caches.

Cách triển khai Singleton trong Go

Dưới đây là 3 cách triển khai Singleton trong Go: cơ bản, thread-safe với mutex, và thread-safe với sync.Once.

1. Basic Singleton

// singleton/singleton.go
package singleton

import "fmt"

type Singleton struct {
    data string
}

var instance *Singleton

func GetInstance() *Singleton {
    if instance == nil {
        instance = &Singleton{data: "Singleton Instance"}
        fmt.Println("Creating new Singleton instance")
    }
    return instance
}

Giải thích:

  • instance là biến package-level giữ instance duy nhất.
  • GetInstance() kiểm tra nếu instance == nil thì tạo mới, nếu không thì trả về instance đã có.
  • Thuộc tính data chỉ để minh họa dữ liệu trong Singleton.

Sử dụng:

package main

import (
    "fmt"
    "yourmodule/singleton"
)

func main() {
    s1 := singleton.GetInstance()
    s2 := singleton.GetInstance()

    fmt.Println(s1 == s2) // true
    fmt.Println(s1.data)  // Singleton Instance
}

Nhược điểm: Không thread-safe, có thể gây race condition trong môi trường đa luồng.

2. Thread-Safe Singleton với Mutex

// singleton/singleton.go
package singleton

import (
    "fmt"
    "sync"
)

type Singleton struct {
    data string
}

var instance *Singleton
var mutex = &sync.Mutex{}

func GetInstance() *Singleton {
    mutex.Lock()
    defer mutex.Unlock()

    if instance == nil {
        instance = &Singleton{data: "Thread-safe Singleton Instance"}
        fmt.Println("Creating new Singleton instance")
    }
    return instance
}

Giải thích:

  • sync.Mutex được dùng để khóa khi tạo instance.
  • mutex.Lock()defer mutex.Unlock() đảm bảo chỉ một goroutine được chạy khối tạo instance.
  • Tránh được race condition nhưng có overhead do lock mỗi lần gọi.

Sử dụng:

package main

import (
    "fmt"
    "sync"
    "yourmodule/singleton"
)

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            s := singleton.GetInstance()
            fmt.Println(s.data)
        }()
    }
    wg.Wait()
}

Kết quả: Chỉ một instance được tạo ra.


3. Thread-Safe Singleton với sync.Once

// singleton/singleton.go
package singleton

import (
    "fmt"
    "sync"
)

type Singleton struct {
    data string
}

var instance *Singleton
var once sync.Once

func GetInstance() *Singleton {
    once.Do(func() {
        instance = &Singleton{data: "Thread-safe Singleton with sync.Once"}
        fmt.Println("Creating new Singleton instance")
    })
    return instance
}

Giải thích:

  • sync.Once đảm bảo hàm khởi tạo chỉ được gọi duy nhất một lần, bất kể bao nhiêu goroutine gọi GetInstance().
  • Hiệu quả hơn mutex vì chỉ check trạng thái một lần.
  • Đây là cách triển khai khuyến nghị trong Go.

Sử dụng:

package main

import (
    "fmt"
    "sync"
    "yourmodule/singleton"
)

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            s := singleton.GetInstance()
            fmt.Println(s.data)
        }()
    }
    wg.Wait()
}

Kết quả: Chỉ một instance được tạo ra.

Nhược điểm của Singleton Pattern

  • Vi phạm Single Responsibility Principle (SRP): Singleton vừa quản lý lifecycle vừa chứa business logic.
  • Tạo coupling và dependency cao: Dễ gây khó khăn khi bảo trì, mở rộng.
  • Khó test: Global state khiến test case ảnh hưởng lẫn nhau, khó mock hoặc thay thế trong unit test.
  • Hạn chế mở rộng: Nếu muốn chuyển từ single instance sang multiple instances (ví dụ load balancing) → phải refactor nhiều.
  • Concurrency issues: Nếu không cẩn thận, có thể tạo nhiều instance trong môi trường đa luồng.
  • Performance bottleneck: Singleton có thể thành điểm nghẽn nếu bị truy cập bởi nhiều thread cùng lúc.

Giải pháp khắc phục

  • Dùng Dependency Injection: Thay vì gọi Singleton toàn cục, truyền instance vào class cần dùng.
  • Định nghĩa Interface cho Singleton: Giúp dễ thay thế hoặc mock khi test.
  • Thiết kế thread-safe: Sử dụng sync.Once trong Go.
  • Tránh lưu trữ mutable state: Hạn chế trạng thái toàn cục dễ thay đổi.
  • Cung cấp reset/override khi test: Đảm bảo test case độc lập.
  • Chỉ dùng khi cần thiết: Đánh giá kỹ trước khi áp dụng.
  • Tách quản lý lifecycle khỏi business logic: Để Singleton chỉ lo quản lý instance.

Best Practices khi dùng Singleton

  • Chỉ dùng cho resource thật sự duy nhất (DB connection, global config).
  • Đảm bảo thread-safety trong môi trường concurrent.
  • Ưu tiên Dependency Injection để giảm coupling.
  • Định nghĩa Singleton qua interface để dễ mở rộng, test.
  • Tránh trạng thái mutable trong Singleton.
  • Document rõ mục đích và phạm vi sử dụng.

Kết luận

Singleton Pattern là công cụ hữu ích trong Go để đảm bảo chỉ có một instance và cung cấp global access.
Tuy nhiên, nó cần được triển khai cẩn thận, đặc biệt trong môi trường concurrent.

Cách triển khai khuyến nghị trong Go: sử dụng sync.Once.

Chuyên mục