Menu
CoddyTech

Mẫu Singleton

Thuộc phần Lập trình hướng đối tượng trong hành trình GO của Coddy. Bài học 86 trên 107.

Mẫu Singleton đảm bảo một kiểu chỉ có một instance trong toàn bộ chương trình và cung cấp một điểm truy cập toàn cục đến instance đó. Mẫu này hữu ích cho các tài nguyên dùng chung như trình quản lý cấu hình, kết nối cơ sở dữ liệu hoặc bộ ghi nhật ký.

Trong Go, ta triển khai Singleton bằng cách kết hợp biến cấp package với sync.Once để đảm bảo quá trình khởi tạo an toàn khi chạy đồng thời:

package config

import "sync"

type Config struct {
    DatabaseURL string
    MaxRetries  int
}

var (
    instance *Config
    once     sync.Once
)

func GetInstance() *Config {
    once.Do(func() {
        instance = &Config{
            DatabaseURL: "localhost:5432",
            MaxRetries:  3,
        }
    })
    return instance
}

sync.Once đảm bảo hàm khởi tạo chỉ chạy đúng một lần, ngay cả khi nhiều goroutine cùng gọi GetInstance(). Mọi lần gọi sau đó đều trả về cùng một instance mà không chạy lại code khởi tạo.

cfg1 := config.GetInstance()
cfg2 := config.GetInstance()
// cfg1 và cfg2 cùng trỏ đến một instance

Hãy dùng Singleton có chừng mực. Dù tiện lợi, Singleton tạo ra trạng thái toàn cục, khiến việc kiểm thử khó hơn và che giấu các phần phụ thuộc. Hãy cân nhắc liệu dependency injection có phù hợp hơn với trường hợp sử dụng của bạn không.

challenge icon

Thử thách

Dễ

Hãy tạo một logger singleton để đảm bảo ứng dụng chỉ có một instance logger! Đây là trường hợp sử dụng kinh điển của mẫu Singleton. Bạn muốn mọi phần trong chương trình dùng chung cấu hình và trạng thái của logger.

Bạn sẽ tổ chức code trong hai tệp:

  • logger.go: Triển khai logger Singleton an toàn khi chạy đa luồng.

    Tạo một struct Logger với hai trường: Prefix (chuỗi) để đặt tiền tố cho thông báo nhật ký và MessageCount (số nguyên) để theo dõi số thông báo đã được ghi nhật ký.

    Dùng các biến cấp package với sync.Once để đảm bảo chỉ tạo một instance. Triển khai hàm GetLogger() để khởi tạo logger với tiền tố mặc định là "[LOG]" và trả về instance singleton.

    Thêm các phương thức sau vào Logger:

    • SetPrefix(prefix string) - thay đổi tiền tố của logger
    • Log(message string) string - tăng số thông báo và trả về chuỗi đã định dạng: [prefix] #[count]: [message]
    • GetCount() int - trả về tổng số thông báo đã được ghi nhật ký
  • main.go: Minh họa rằng nhiều lần gọi GetLogger() đều trả về cùng một instance.

    Đọc một giá trị tiền tố mới, sau đó đọc số lượng thông báo và từng thông báo cần ghi nhật ký.

    Lấy instance logger, đặt tiền tố tùy chỉnh, sau đó ghi nhật ký từng thông báo và in kết quả. Sau khi ghi nhật ký tất cả thông báo, lấy lại instance logger để chứng minh đó vẫn là cùng một instance, rồi in tổng số thông báo.

Đầu vào sẽ có định dạng như sau:

  • Dòng 1: Tiền tố tùy chỉnh cần đặt
  • Dòng 2: Số lượng thông báo
  • Các dòng tiếp theo: Từng thông báo cần ghi nhật ký

Ví dụ, với đầu vào:

[APP]
3
Server started
User connected
Request processed

Đầu ra sẽ là:

[APP] #1: Server started
[APP] #2: User connected
[APP] #3: Request processed
Total messages logged: 3

Với đầu vào:

[DEBUG]
2
Initializing cache
Cache ready

Đầu ra sẽ là:

[DEBUG] #1: Initializing cache
[DEBUG] #2: Cache ready
Total messages logged: 2

Với đầu vào:

[ERROR]
1
Connection failed

Đầu ra sẽ là:

[ERROR] #1: Connection failed
Total messages logged: 1

Điểm mấu chốt là dù bạn gọi GetLogger() bao nhiêu lần, bạn vẫn luôn nhận được cùng một instance với trạng thái dùng chung. Số lượng thông báo vẫn được giữ nguyên vì chỉ có một Logger tồn tại!

Tự làm thử

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)

	// Đọc tiền tố tùy chỉnh
	prefix, _ := reader.ReadString('\n')
	prefix = strings.TrimSpace(prefix)

	// Đọc số lượng thông điệp
	countStr, _ := reader.ReadString('\n')
	countStr = strings.TrimSpace(countStr)
	count, _ := strconv.Atoi(countStr)

	// Đọc từng thông điệp
	messages := make([]string, count)
	for i := 0; i < count; i++ {
		msg, _ := reader.ReadString('\n')
		messages[i] = strings.TrimSpace(msg)
	}

	// TODO: Lấy instance của logger bằng GetLogger()

	// TODO: Đặt tiền tố tùy chỉnh bằng SetPrefix()

	// TODO: Ghi log từng thông điệp và in ra kết quả

	// TODO: Lấy lại instance của logger (để chứng minh đó vẫn là instance cũ)
	// và in ra tổng số thông điệp theo định dạng: "Total messages logged: X"
}
quiz iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Lập trình hướng đối tượng

Tự luyện tập: Trình biên dịch Go online