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

Chú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 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)

Ý 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

Ý 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

Ý 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-checkmutex 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