Singleton
Singleton là một mô hình Creational, đảm bảo rằng chỉ có một đối tượng của loại hình này tồn tại và cung cấp một điểm truy cập duy nhất cho nó cho bất kỳ mã nào khác.
Singleton có gần như những ưu và nhược điểm như các biến Global. Mặc dù họ siêu chủ yếu, nhưng họ phá vỡ tính mô-đun của mã của bạn.
Bạn có thể chỉ cần sử dụng một lớp phụ thuộc vào một singleton trong một số bối cảnh khác, mà không mang qua singleton đến bối cảnh khác. Hầu hết thời gian, giới hạn này xuất hiện trong quá trình tạo ra các bài kiểm tra đơn vị.
Conceptual Example
Thông thường, một thể hiện singleton được tạo khi cấu trúc được khởi tạo đầu tiên. Để thực hiện điều này, chúng tôi xác định getInstance phương pháp trên cấu trúc mã. Phương pháp này sẽ chịu trách nhiệm tạo và trả lại phiên bản Singleton. Sau khi được tạo, cùng một trường hợp singleton sẽ được trả lại mỗi khi getInstance được gọi.
Một số điểm đáng chú ý:
Ở đầu có một nil-check để đảm bảo rằng singleInstance
trống trong lần gọi đầu tiên. Điều này nhằm tránh phải thực hiện các thao tác lock tốn kém mỗi lần phương thức getInstance
được gọi. Nếu kiểm tra này thất bại, có nghĩa là trường singleInstance
đã được khởi tạo.
Struct singleInstance
sẽ được tạo ra bên trong lock.
Có một lần nil-check khác sau khi lock được acquire. Điều này để đảm bảo rằng nếu có nhiều goroutine cùng vượt qua lần kiểm tra đầu tiên, thì chỉ có duy nhất một goroutine được phép tạo ra instance của singleton. Nếu không có kiểm tra này, tất cả goroutine đều sẽ tự tạo instance riêng của struct singleton.
single.go: Singleton
package main
import (
"fmt"
"sync"
)
var (
lock sync.Mutex
singleInstance *single
)
type single struct{}
// double checked pattern
func getInstance() *single {
if singleInstance != nil {
fmt.Println("Single instance already created.")
return singleInstance
}
lock.Lock()
defer lock.Unlock()
if singleInstance == nil {
fmt.Println("Creating single instance now.")
singleInstance = &single{}
return singleInstance
}
fmt.Println("Single instance already created.")
return singleInstance
}
sync.Once
đảm bảo code trong Do
chỉ chạy đúng một lần, thread-safe, không cần check nil
hai lần.
var (
once sync.Once
singleInstance *single
)
func getInstance() *single {
once.Do(func() {
fmt.Println("Creating single instance now.")
singleInstance = &single{}
})
return singleInstance
}
main.go: Client code
package main
import (
"fmt"
)
func main() {
for i := 0; i < 30; i++ {
go getInstance()
}
// Scanln is similar to Scan, but stops scanning at a newline and
// after the final item there must be a newline or EOF.
fmt.Scanln()
}
output.txt: Execution result
Creating single instance now.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Single instance already created.
Another Example
Ok, mình sẽ dịch đoạn này sang tiếng Việt nhưng vẫn giữ ngữ nghĩa IT chuẩn xác:
Có những phương pháp khác để tạo singleton instance trong Go:
1. init
function
init
functionChúng ta có thể tạo instance ngay bên trong hàm init
. Cách này chỉ phù hợp nếu việc khởi tạo sớm (early initialization) của instance là chấp nhận được. Hàm init
chỉ được gọi một lần duy nhất cho mỗi file trong một package, vì vậy ta có thể chắc chắn rằng chỉ có một instance được tạo ra.
2. sync.Once
sync.Once
sync.Once
sẽ đảm bảo một đoạn code chỉ được thực thi một lần duy nhất. Xem ví dụ code dưới đây:
package main
import (
"fmt"
"sync"
)
type singleton struct{}
var instance *singleton
var once sync.Once
func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{}
fmt.Println("Singleton instance created")
})
return instance
}
func main() {
// Gọi nhiều lần nhưng chỉ tạo 1 instance
s1 := GetInstance()
s2 := GetInstance()
if s1 == s2 {
fmt.Println("Both variables contain the same instance")
}
}
Ở đây, once.Do
sẽ đảm bảo rằng logic khởi tạo instance chỉ chạy một lần, bất kể có bao nhiêu goroutine cùng gọi GetInstance()
.
1. Double-Checked Locking (kết hợp nil-check
+ sync.Mutex
)
nil-check
+ sync.Mutex
)Ý tưởng:
Dùng
nil-check
trước khi lock để tránh tốn chi phí lock mỗi lần gọi.Nếu instance chưa có, thì lock lại → kiểm tra thêm 1 lần nữa → tạo instance.
Ưu điểm:
Hiệu năng ổn định (chỉ lock khi cần).
Chắc chắn an toàn trong môi trường multi-goroutine.
Nhược điểm:
Code phức tạp hơn (
mutex
,nil-check
2 lần).Dễ sai sót nếu không viết cẩn thận.
Ít được dùng trong Go vì đã có
sync.Once
.
Khi dùng:
Khi bạn cần kiểm soát chi tiết cách lock và khởi tạo.
Các tình huống rất nhạy về performance (dù đa số trường hợp
sync.Once
đã đủ tốt).
2. init
Function
init
FunctionÝ tưởng:
Dùng
init()
để khởi tạo instance ngay khi package được load.init()
luôn chạy 1 lần duy nhất.
Ưu điểm:
Đơn giản, dễ viết, ít code.
Đảm bảo an toàn vì Go runtime gọi
init
chỉ 1 lần.
Nhược điểm:
Khởi tạo sớm (eager initialization): instance được tạo ra ngay cả khi không dùng đến → tốn tài nguyên.
Không linh hoạt (không thể trì hoãn việc tạo instance cho đến khi thực sự cần).
Khi dùng:
Khi chắc chắn instance luôn luôn cần trong vòng đời chương trình.
Ví dụ: logger global, config global mà app luôn phải có.
3. sync.Once
sync.Once
Ý tưởng:
Dùng
once.Do(func() { ... })
để khởi tạo.Hàm trong
Do()
được gọi duy nhất một lần, kể cả nhiều goroutine gọi cùng lúc.
Ưu điểm:
Ngắn gọn, dễ đọc, clean code.
An toàn tuyệt đối với concurrency.
Không cần viết
nil-check
vàmutex
thủ công.
Nhược điểm:
Không phù hợp nếu bạn muốn reset hoặc tái khởi tạo instance (vì
sync.Once
chỉ chạy 1 lần trọn đời).
Khi dùng:
Khi cần lazy initialization (khởi tạo khi cần).
Đây là cách chuẩn & idiomatic nhất trong Go để implement Singleton.
🔑 Tổng kết (Recommendation)
init()
→ khi chắc chắn instance luôn cần và khởi tạo sớm không gây phí.sync.Once
→ cách idiomatic, recommended trong Go, nên dùng trong hầu hết các trường hợp.Double-Checked Locking → chỉ dùng khi có lý do đặc biệt (ví dụ cần kiểm soát
mutex
chi tiết hoặc hiệu năng cực kỳ nhạy).
👉 Với dự án thực tế trong Go, sync.Once
gần như luôn là lựa chọn tốt nhất.
Trong backend, Singleton (hoặc sync.Once
) thường dùng cho infrastructure layer:
DB connection
Cache client
Message broker
Logger
Config loader
SDK client
Last updated