Singleton

  • Kiến thức nền tảng

  • Tình huống sử dụng thực tế

  • Cách implement chuẩn production (thread-safe, lazy, với DI)

  • Anti-pattern cần tránh

  • Benchmark và kiểm thử đơn vị


💡 1. Khi nào cần dùng Singleton?

✅ Dùng khi:

  • Chỉ cần một instance duy nhất trong toàn app (vd: config, logger, db connection pool)

  • Tránh khởi tạo lại đối tượng nặng hoặc tốn chi phí (vd: Redis pool, Kafka producer)

  • Đảm bảo thread-safe access trong concurrent Golang apps

❌ Không dùng khi:

  • Mỗi request cần state riêng biệt

  • Muốn dễ test unit (singleton khó mock nếu không inject được)


🧱 2. Các cách implement Singleton trong Go

⚙️ Cách 1: Thread-safe với sync.Once (chuẩn production)

package config

import (
    "sync"
)

type Config struct {
    DatabaseURL string
    Port        int
}

var (
    instance *Config
    once     sync.Once
)

func GetInstance() *Config {
    once.Do(func() {
        instance = &Config{
            DatabaseURL: "postgres://localhost:5432/db",
            Port:        8080,
        }
    })
    return instance
}

🧠 Giải thích:

  • sync.Once đảm bảo hàm Do chỉ được chạy 1 lần, dù nhiều goroutine cùng gọi

  • Thread-safe tuyệt đối, không cần lock thủ công

  • Lazy-init: chỉ khởi tạo khi lần đầu được gọi


🧪 3. Test đơn vị singleton

func TestSingleton(t *testing.T) {
    c1 := GetInstance()
    c2 := GetInstance()
    if c1 != c2 {
        t.Error("Expected same instance, got different instances")
    }
}

🔥 4. Tình huống thực tế

🧩 Use Case: Logger Service

package logger

import (
    "log"
    "os"
    "sync"
)

type Logger struct {
    *log.Logger
}

var (
    instance *Logger
    once     sync.Once
)

func GetLogger() *Logger {
    once.Do(func() {
        instance = &Logger{
            Logger: log.New(os.Stdout, "[APP] ", log.LstdFlags),
        }
    })
    return instance
}

Sử dụng:

logger := logger.GetLogger()
logger.Println("Service started...")

🧨 5. Anti-pattern cần tránh

❌ Dùng init() để khởi tạo Singleton:

var instance = &Config{} // KHÔNG nên
  • Khởi tạo eager (luôn khởi tạo kể cả không dùng)

  • Không thread-safe nếu init phức tạp

  • Khó test/mock trong unit test


🔧 6. Singleton với Dependency Injection (Pro)

Kỹ thuật: inject singleton vào service layer

type DB struct {
    Conn *sql.DB
}

type UserService struct {
    db *DB
}

func NewUserService(db *DB) *UserService {
    return &UserService{db: db}
}

Tại main.go:

func main() {
    db := database.GetInstance()
    userSvc := NewUserService(db)
    userSvc.DoSomething()
}

➡️ Điều này giúp dễ mock db trong unit test thay vì hardcoded singleton trong UserService.


📈 7. Benchmark sơ bộ

func BenchmarkGetInstance(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = GetInstance()
    }
}

Với sync.Once, chi phí truy xuất sau lần đầu là cực thấp (vài ns), không cần tối ưu thêm.


✅ Tổng kết checklist

Mục
Đạt

Dùng sync.Once để đảm bảo thread-safe lazy init

Không khởi tạo trước bằng init()

Inject singleton vào services để dễ test

Tránh lưu state bên trong singleton (nếu có concurrency)

Viết test để kiểm tra instance


Last updated