- 1
 - 512
 
Golang: Kỹ thuật sử dụng Go sync
| May 16, 2025 | 5 min read🌀 Hiểu đúng và dùng chuẩnsync.Once trong Golang
Trong thế giới Go, nơi concurrency (xử lý song song) là “đặc sản”, việc đảm bảo một đoạn code chỉ chạy đúng 1 lần là điều cực kỳ quan trọng. Từ việc khởi tạo kết nối DB, load file cấu hình, cho đến tạo singleton, bạn đều cần một cách an toàn, gọn gàng và hiệu quả để “chạy một lần duy nhất”.
Đó chính là lý do sync.Once ra đời — một công cụ nhỏ nhưng cực kỳ mạnh mẽ trong kho vũ khí của lập trình viên Go.
🚀sync.Oncelà gì?
sync.Once là một synchronization primitive nằm trong package sync.
Mục đích của nó:
Đảm bảo rằng một hàm nhất định chỉ được thực thi một lần duy nhất, cho dù có bao nhiêu goroutine cùng gọi nó cùng lúc.
Cụ thể:
Phương thức duy nhất: Do(f func())
Đảm bảo: Hàm f chỉ được gọi đúng một lần.
Thread-safe: An toàn tuyệt đối trong môi trường nhiều goroutine.
🧱 Khác gì với Mutex hay Channel?
Mutex có thể khóa/mở nhiều lần.
sync.Once chỉ chạy một lần duy nhất trong suốt vòng đời chương trình.
Sau lần đầu, các lần gọi Do() tiếp theo gần như là no-op (siêu nhanh).
🔧 Khi nào dùngsync.Once?
| Tình huống | Mục đích | 
|---|---|
| Khởi tạo kết nối database | Tạo connection/pool duy nhất | 
| Cấu hình ứng dụng | Chỉ load 1 lần | 
| Singleton pattern | Đảm bảo instance duy nhất | 
| Khởi tạo cache | Tránh load lại dữ liệu | 
| Lazy initialization | Chỉ tạo tài nguyên khi thật sự cần | 
💡 Ví dụ 1: Singleton Pattern
Mẫu “kinh điển” nhất khi học sync.Once — đảm bảo chỉ tạo một instance duy nhất của struct.
package main
import (
	"fmt"
	"sync"
)
type Singleton struct {
	data string
}
var instance *Singleton
var once sync.Once
func GetInstance() *Singleton {
	once.Do(func() {
		fmt.Println("Creating Singleton instance")
		instance = &Singleton{data: "I'm the only one!"}
	})
	return instance
}
func main() {
	for i := 0; i < 5; i++ {
		go func() {
			fmt.Printf("%p\n", GetInstance())
		}()
	}
	fmt.Scanln()
}✅ Điểm hay:
Thread-safe, tránh race condition.
Lazy initialization – chỉ tạo khi cần.
Sau lần đầu, các goroutine khác gần như chạy tức thì.
| Cách triển khai | Thread-safe | Lazy-init | Độ phức tạp | 
|---|---|---|---|
| sync.Once | ✅ | ✅ | Thấp | 
| Mutex | ✅ | ✅ | Trung bình | 
| init() | ✅ | ❌ | Thấp | 
| Biến global | ❌ | ❌ | Rất thấp | 
⚙️ Ví dụ 2: Lazy Initialization
Trong thực tế, có nhiều tài nguyên rất tốn chi phí khởi tạo, ví dụ như kết nối database, hay load model AI.
Bạn không muốn khởi tạo sớm, mà chỉ làm khi thật sự cần – và chỉ làm 1 lần.
package main
import (
	"database/sql"
	"fmt"
	"log"
	"sync"
	_ "github.com/lib/pq"
)
type DatabaseConnection struct {
	db *sql.DB
}
var (
	dbConn *DatabaseConnection
	once   sync.Once
)
func GetDatabaseConnection() (*DatabaseConnection, error) {
	var initErr error
	once.Do(func() {
		fmt.Println("Initializing database connection...")
		db, err := sql.Open("postgres", "user=pqgotest dbname=pqgotest sslmode=disable")
		if err != nil {
			initErr = fmt.Errorf("failed to open database: %w", err)
			return
		}
		if err = db.Ping(); err != nil {
			initErr = fmt.Errorf("failed to ping database: %w", err)
			return
		}
		dbConn = &DatabaseConnection{db: db}
	})
	if initErr != nil {
		return nil, initErr
	}
	return dbConn, nil
}
func main() {
	var wg sync.WaitGroup
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			conn, err := GetDatabaseConnection()
			if err != nil {
				log.Printf("Goroutine %d: %v\n", id, err)
				return
			}
			log.Printf("Goroutine %d: Got connection %p\n", id, conn)
		}(i)
	}
	wg.Wait()
}🧠 Lợi ích:
Hiệu suất cao: Tạo tài nguyên khi thật sự cần.
An toàn: Không lo race khi nhiều goroutine cùng init.
Tách biệt logic init: Code sạch, dễ bảo trì.
| Chiến lược | Ưu điểm | Nhược điểm | 
|---|---|---|
| Eager init | Đơn giản | Tốn tài nguyên | 
| Lazy (no sync) | Nhanh | Không an toàn | 
| Lazy (sync.Once) | Nhanh + an toàn | Không thể re-init | 
| Lazy (mutex) | Có thể reinit | Code phức tạp hơn | 
🏗️ Ứng dụng thực tế
1️⃣ Database Pool
var (
	dbPool   *sql.DB
	poolOnce sync.Once
)
func GetDBPool() (*sql.DB, error) {
	var err error
	poolOnce.Do(func() {
		dbPool, err = sql.Open("postgres", "dsn_here")
		if err != nil { return }
		dbPool.SetMaxOpenConns(25)
		dbPool.SetConnMaxLifetime(5 * time.Minute)
	})
	if err != nil { return nil, err }
	return dbPool, nil
}🟢 Đảm bảo pool chỉ tạo một lần, thread-safe, hiệu quả.
2️⃣ Load Config File
type Config struct {
	APIKey string `json:"api_key"`
	Debug  bool   `json:"debug"`
}
var (
	config     *Config
	configOnce sync.Once
)
func GetConfig() (*Config, error) {
	var err error
	configOnce.Do(func() {
		file, e := os.Open("config.json")
		if e != nil {
			err = e
			return
		}
		defer file.Close()
		config = &Config{}
		err = json.NewDecoder(file).Decode(config)
	})
	if err != nil {
		return nil, err
	}
	return config, nil
}
3️⃣ Plugin Initialization
type Plugin struct {
	Name        string
	initialized bool
	initOnce    sync.Once
}
func (p *Plugin) Initialize() error {
	var err error
	p.initOnce.Do(func() {
		fmt.Printf("Initializing plugin %s...\n", p.Name)
		if p.Name == "BadPlugin" {
			err = fmt.Errorf("failed to init plugin %s", p.Name)
		}
		p.initialized = true
	})
	return err
}
🧾 Kết luận
sync.Once là công cụ “nhỏ mà có võ” giúp bạn:
Quản lý tài nguyên an toàn trong môi trường concurrent.
Đơn giản hóa logic init mà vẫn đảm bảo hiệu năng cao.
Tránh các race condition khó chịu khi khởi tạo.
“Do it once, and only once.”
— triết lý đơn giản, nhưng lại là chìa khóa của những hệ thống Go concurrent ổn định.
🎯 Tóm lại:
Dùng sync.Once khi bạn cần một thao tác chạy đúng 1 lần, thread-safe, và hiệu quả — đặc biệt trong môi trường nhiều goroutine như API server, worker pool, hay caching system.