单例模式
本课属于 Coddy GO 学习旅程的“面向对象编程”单元。第 86 课,共 107 课。
单例模式可确保某个类型在整个程序中只有一个实例,并提供一个全局访问入口。这适用于配置管理器、数据库连接或日志记录器等共享资源。
在 Go 中,我们使用包级变量和 sync.Once 来实现 Singleton,确保初始化是线程安全的:
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 可确保初始化函数恰好运行一次,即使多个 goroutine 同时调用 GetInstance()。之后的每次调用都会返回同一个实例,而不会再次执行初始化代码。
cfg1 := config.GetInstance()
cfg2 := config.GetInstance()
// cfg1 和 cfg2 指向同一个实例谨慎使用 Singleton。虽然它很方便,但会引入全局状态,可能让测试更困难,也会隐藏依赖关系。考虑一下依赖注入是否更适合你的使用场景。
挑战
简单让我们构建一个 Logger 单例,确保整个应用中只存在一个日志记录器实例!这是单例模式的经典用例。你希望程序的所有部分共享相同的日志记录器配置和状态。
你将把代码分成两个文件:
logger.go:实现线程安全的单例日志记录器。创建一个
Logger结构体,包含两个字段:Prefix(string),用于设置日志消息前缀;以及MessageCount(int),用于记录已记录的消息数量。使用包级变量和
sync.Once确保只创建一个实例。实现GetLogger()函数,用默认前缀"[LOG]"初始化日志记录器,并返回这个单例实例。为 Logger 添加以下方法:
SetPrefix(prefix string)- 更改日志记录器的前缀Log(message string) string- 增加消息计数,并返回格式化字符串:[prefix] #[count]: [message]GetCount() int- 返回已记录的消息总数
main.go:演示多次调用GetLogger()时都会返回同一个实例。读取新的前缀值,然后读取消息数量,接着读取每条要记录的消息。
获取日志记录器实例,设置自定义前缀,然后记录每条消息并打印结果。记录完所有消息后,再次获取日志记录器实例(以证明它是同一个实例),并打印消息总数。
输入如下:
- 第 1 行:要设置的自定义前缀
- 第 2 行:消息数量
- 后续各行:每条要记录的消息
例如,给定:
[APP]
3
Server started
User connected
Request processed你的输出应为:
[APP] #1: Server started
[APP] #2: User connected
[APP] #3: Request processed
Total messages logged: 3再例如,给定:
[DEBUG]
2
Initializing cache
Cache ready你的输出应为:
[DEBUG] #1: Initializing cache
[DEBUG] #2: Cache ready
Total messages logged: 2再例如,给定:
[ERROR]
1
Connection failed你的输出应为:
[ERROR] #1: Connection failed
Total messages logged: 1关键在于,无论你调用 GetLogger() 多少次,得到的始终都是同一个实例,并且它共享同一份状态。消息计数会一直保留,因为始终只存在一个 Logger!
亲手试试
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// 读取自定义前缀
prefix, _ := reader.ReadString('\n')
prefix = strings.TrimSpace(prefix)
// 读取消息数量
countStr, _ := reader.ReadString('\n')
countStr = strings.TrimSpace(countStr)
count, _ := strconv.Atoi(countStr)
// 逐条读取消息
messages := make([]string, count)
for i := 0; i < count; i++ {
msg, _ := reader.ReadString('\n')
messages[i] = strings.TrimSpace(msg)
}
// TODO:使用 GetLogger() 获取日志记录器实例
// TODO:使用 SetPrefix() 设置自定义前缀
// TODO:记录每条消息并打印结果
// TODO:再次获取日志记录器实例(以证明它是同一个实例)
// 并按以下格式打印消息总数:"Total messages logged: X"
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: Go 在线编译器