• 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ốngMục đích
Khởi tạo kết nối databaseTạo connection/pool duy nhất
Cấu hình ứng dụngChỉ load 1 lần
Singleton patternĐảm bảo instance duy nhất
Khởi tạo cacheTránh load lại dữ liệu
Lazy initializationChỉ 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 khaiThread-safeLazy-initĐộ phức tạp
sync.OnceThấp
MutexTrung bình
init()Thấp
Biến globalRấ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ểmNhược điểm
Eager initĐơn giảnTốn tài nguyên
Lazy (no sync)NhanhKhông an toàn
Lazy (sync.Once)Nhanh + an toànKhông thể re-init
Lazy (mutex)Có thể reinitCode 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.