Design Pattern Series - Singleton Pattern (Golang)

Go Design Pattern Series Jun 3, 2025

Previous post in series: Abstract Factory Pattern

Singleton Pattern là một trong những mẫu thiết kế thuộc nhóm Creational Patterns (Nhóm khởi tạo), giúp đảm bảo rằng một struct chỉ có duy nhất một thực thể (instance) trong suốt vòng đời của ứng dụng và cung cấp một điểm truy cập toàn cục (global access) đến thực thể đó.

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

Các trường hợp sử dụng Singleton

Singleton thường được áp dụng khi bạn muốn:

  • Kiểm soát tài nguyên: Đảm bảo chỉ có một thực thể duy nhất quản lý một tài nguyên cụ thể.
  • Truy cập toàn cục: Cung cấp một điểm truy cập duy nhất cho các tài nguyên dùng chung.
  • Tối ưu hiệu suất: Tránh việc khởi tạo lặp đi lặp lại các đối tượng nặng (heavy instances).

Ví dụ thực tế:

  • Quản lý kết nối cơ sở dữ liệu (Database Connection).
  • Quản lý cấu hình ứng dụng (Configuration Manager).
  • Dịch vụ ghi log (Logging Service).
  • Bộ nhớ đệm (Cache).

Triển khai Singleton trong Go

Dưới đây là 3 cách phổ biến để triển khai Singleton: Cách cơ bản, Thread-safe với Mutex, và cách tối ưu nhất là Thread-safe với sync.Once.

1. Basic Singleton (Chưa an toàn)

// 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("Đang tạo thực thể Singleton mới")
    }
    return instance
}
  • Giải thích: Sử dụng một biến cấp package để giữ thực thể duy nhất. GetInstance() kiểm tra nếu chưa có thì mới tạo.
  • Nhược điểm: Không an toàn trong môi trường đa luồng (not thread-safe). Nhiều goroutine có thể cùng vượt qua kiểm tra nil và tạo ra nhiều thực thể khác nhau.

2. Thread-Safe Singleton với Mutex

// singleton/singleton.go
package singleton

import (
    "fmt"
    "sync"
)

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("Đang tạo thực thể Singleton mới")
    }
    return instance
}
  • Giải thích: Sử dụng sync.Mutex để khóa luồng khi khởi tạo. Đảm bảo chỉ một goroutine được quyền tạo thực thể.
  • Nhược điểm: Gây ảnh hưởng đến hiệu suất (overhead) vì mọi lời gọi hàm đều phải chờ khóa, ngay cả khi thực thể đã tồn tại.

3. Thread-Safe Singleton với sync.Once (Khuyên dùng)

// 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 với sync.Once"}
        fmt.Println("Đang tạo thực thể Singleton mới")
    })
    return instance
}
  • Giải thích: sync.Once đảm bảo hàm bên trong chỉ chạy đúng một lần duy nhất, bất kể có bao nhiêu goroutine gọi nó.
  • Ưu điểm: Đây là cách triển khai tối ưu và được khuyên dùng nhất trong Go vì hiệu suất cao và an toàn tuyệt đối.

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

Mặc dù hữu ích, Singleton cũng mang lại một số phiền toái nếu bị lạm dụng:

  1. Vi phạm nguyên tắc Đơn trách nhiệm (SRP): Singleton vừa quản lý vòng đời của chính nó, vừa chứa logic nghiệp vụ.
  2. Độ phụ thuộc cao (Tight Coupling): Khó bảo trì và mở rộng do trạng thái toàn cục.
  3. Khó Unit Test: Trạng thái toàn cục khiến các test case ảnh hưởng lẫn nhau, khó thay thế (mock) trong quá trình kiểm thử.
  4. Điểm nghẽn hiệu suất: Có thể trở thành "nút thắt cổ chai" nếu bị truy cập dồn dập bởi quá nhiều luồng cùng lúc.

Giải pháp và Best Practices

Để sử dụng Singleton hiệu quả, hãy cân nhắc các giải pháp sau:

  • Sử dụng Dependency Injection: Thay vì gọi trực tiếp Singleton toàn cục, hãy truyền thực thể đó vào thông qua tham số của hàm hoặc struct cần sử dụng.
  • Định nghĩa Interface: Giúp dễ dàng mock hoặc thay thế thực thể khi viết Unit Test.
  • Thiết kế an toàn: Luôn sử dụng sync.Once trong Go.
  • Tách biệt logic: Hãy để Singleton chỉ đóng vai trò quản lý thực thể, còn logic nghiệp vụ nên nằm ở chỗ khác.
  • Chỉ dùng khi thực sự cần thiết: Đánh giá kỹ liệu tài nguyên đó có thực sự cần là duy nhất hay không trước khi áp dụng.

Tổng kết

Singleton Pattern là một công cụ hữu ích trong Go để đảm bảo sự tồn tại duy nhất của một tài nguyên và cung cấp khả năng truy cập thuận tiện. Tuy nhiên, nó cần được triển khai cẩn thận, đặc biệt là trong môi trường xử lý đồng thời.

Cách tốt nhất để triển khai Singleton trong Go là sử dụng sync.Once.

Cảm ơn bạn đã đọc bài viết! Hy vọng bài viết giúp bạn nắm vững cách quản lý thực thể duy nhất trong ứng dụng của mình

Tags