Menu
CoddyTech

单例模式

本课属于 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。虽然它很方便,但会引入全局状态,可能让测试更困难,也会隐藏依赖关系。考虑一下依赖注入是否更适合你的使用场景。

challenge icon

挑战

简单

让我们构建一个 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"
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Go 在线编译器