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 instanceHã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.
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
Loggervớ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àmGetLogger()để 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 loggerLog(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ọiGetLogger()đề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: 3Vớ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: 2Vớ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"
}
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
1Nền tảng OOP trong Go
Tệp bên ngoàiWorkspace và module trong GoPackage và importTên được export và không được exportGiới thiệu về OOP trong GoStruct như lớpĐịnh nghĩa phương thức cho structReceiver kiểu con trỏ và kiểu giá trịKhởi tạo structHàm khởi tạoÔn tập: máy tính đơn giản4Interface
Giới thiệu về interfaceTriển khai ngầm địnhInterface như một hợp đồngInterface rỗng (any)Khẳng định kiểuswitch kiểuKết hợp interfaceInterface Stringer và ErrorÔn tập: Máy tính hình học7Tính đóng gói
Trường được xuất và không được xuấtĐóng gói ở cấp packagePhương thức getter và setterẨn thông tin trong GoÔn tập: hồ sơ học sinh10Generics (Go 1.18 trở lên)
Giới thiệu về GenericsTham số kiểuRàng buộc kiểuStruct tổng quátGiải pháp thay thế cho phương thức tổng quátÔn tập: tập hợp tổng quát13Các mẫu thiết kế phần 1
Giới thiệu về mẫu thiết kếMẫu SingletonMẫu FactoryMẫu Abstract FactoryMẫu ObserverMẫu Strategy2Tìm hiểu chuyên sâu về kiểu dữ liệu và struct
Kiểu cơ bản và kiểu tổng hợpĐịnh nghĩa kiểu dữ liệu tùy chỉnhThẻ structStruct ẩn danhStruct lồng nhauGiá trị zero và giá trị mặc địnhÔn tập: sổ liên lạc5Ưu tiên kết hợp hơn kế thừa
Vì sao Go không có kế thừaNhập môn nhúng structTự động nâng phương thứcNhúng nhiều structNhúng và kết tậpChe khuất phương thức được nhúngÔn tập: hệ phân cấp nhân viên8Xử lý lỗi và lập trình hướng đối tượng
Giao diện errorCác kiểu lỗi tùy chỉnhBọc lỗi với fmt.ErrorfLỗi sentinelerrors.Is() và errors.As()Panic, defer và recoverÔn tập: trình phân tích tệp3Con trỏ và bộ nhớ
Cơ bản về con trỏ trong GoCon trỏ đến structTruyền theo giá trị và tham chiếuHàm new()Thu gom rác trong GoÔn tập: xây dựng danh sách liên kết6Đa hình trong Go
Đa hình qua interfaceĐịnh kiểu vịt trong GoQuy tắc thỏa mãn interfaceCác tập hợp đa hìnhTiêm phụ thuộcÔn tập: bộ xử lý thanh toán9Lập trình đồng thời và lập trình hướng đối tượng
Cơ bản về goroutineChannel và giao tiếpChannel có bộ đệm và không có bộ đệmCâu lệnh selectsync.Mutex và sync.RWMutexsync.WaitGroupThiết kế struct an toàn luồngÔn tập: worker poolTự luyện tập: Trình biên dịch Go online