什么是单例模式?
单例模式是一种创建型设计模式,确保一个类只有一个实例,并提供全局访问点。它适用于需要统一管理资源的场景,例如日志管理、配置管理或数据库连接池。
核心要素:
- 私有构造函数(Go中通过包控制)。
- 全局唯一实例。
- 全局访问点。
- 线程安全(在并发场景中)。
为什么在Go中使用单例模式?
Go语言以简洁著称,但单例模式在以下场景中非常有用:
- 日志管理:全局共享一个日志实例,避免重复创建。
- 配置管理:统一加载和访问配置。
- 资源控制:如数据库连接池,减少资源浪费。
本文将通过一个日志管理器的真实例子,展示如何在Go中实现单例模式。
非线程安全的单例实现
我们先实现一个简单的单例模式,适用于单线程或明确初始化的场景。
代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
package logger
import (
"fmt"
"time"
)
// Logger 结构体,表示日志管理器
type Logger struct {
level string // 日志级别,例如 "INFO", "ERROR"
}
// Log 方法,记录一条日志
func (l *Logger) Log(message string) {
timestamp := time.Now().Format("2006-01-02 15:04:05")
fmt.Printf("[%s] %s: %s\n", timestamp, l.level, message)
}
// 私有变量,存储唯一实例
var logger *Logger
// GetLogger 返回单例实例
func GetLogger() *Logger {
if logger == nil {
logger = &Logger{level: "INFO"}
}
return logger
}
|
使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
package main
import (
"yourmodule/logger"
)
func main() {
log1 := logger.GetLogger()
log1.Log("Application started")
log2 := logger.GetLogger()
log2.Log("Processing data")
fmt.Printf("log1 == log2: %v\n", log1 == log2) // 输出 true
}
|
讲解
logger
是包级私有变量,外部无法直接访问。
GetLogger
在logger
为nil
时初始化实例。
- 局限性:在多goroutine场景下,可能会多次初始化。
线程安全的单例实现
Go程序通常是并发的,我们需要确保单例初始化的线程安全。Go的sync.Once
是实现线程安全单例的理想工具。
代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
package logger
import (
"fmt"
"sync"
"time"
)
// Logger 结构体,表示日志管理器
type Logger struct {
level string // 日志级别
}
// Log 方法,记录一条日志
func (l *Logger) Log(message string) {
timestamp := time.Now().Format("2006-01-02 15:04:05")
fmt.Printf("[%s] %s: %s\n", timestamp, l.level, message)
}
// 私有变量和同步控制
var logger *Logger
var once sync.Once
// GetLogger 返回线程安全的单例实例
func GetLogger() *Logger {
once.Do(func() {
logger = &Logger{level: "INFO"}
})
return logger
}
|
使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package main
import (
"sync"
"yourmodule/logger"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
log := logger.GetLogger()
log.Log(fmt.Sprintf("Message from goroutine %d", id))
}(i)
}
wg.Wait()
log1 := logger.GetLogger()
log2 := logger.GetLogger()
fmt.Printf("log1 == log2: %v\n", log1 == log2) // 输出 true
}
|
讲解
sync.Once
的Do
方法确保logger
只初始化一次。
- 适合高并发场景,例如Web服务中的日志记录。
真实项目:增强版日志管理器
在实际项目中,日志管理器需要更多功能,例如写入文件、支持不同日志级别等。我们扩展Logger
并保持单例模式。
代码实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
package logger
import (
"fmt"
"os"
"sync"
"time"
)
// Logger 结构体,增强版日志管理器
type Logger struct {
level string // 日志级别
file *os.File // 日志文件
mu sync.Mutex // 保护文件写入
}
// Log 方法,记录日志到控制台和文件
func (l *Logger) Log(message string) {
l.mu.Lock()
defer l.mu.Unlock()
timestamp := time.Now().Format("2006-01-02 15:04:05")
logMessage := fmt.Sprintf("[%s] %s: %s\n", timestamp, l.level, message)
fmt.Print(logMessage)
if l.file != nil {
l.file.WriteString(logMessage)
}
}
// Close 关闭日志文件
func (l *Logger) Close() {
l.mu.Lock()
defer l.mu.Unlock()
if l.file != nil {
l.file.Close()
}
}
var logger *Logger
var once sync.Once
// GetLogger 返回线程安全的单例实例
func GetLogger() *Logger {
once.Do(func() {
file, err := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
fmt.Printf("Failed to open log file: %v\n", err)
}
logger = &Logger{
level: "INFO",
file: file,
}
})
return logger
}
|
使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package main
import (
"yourmodule/logger"
)
func main() {
log := logger.GetLogger()
log.Log("Application started")
log.Log("Processing data")
defer log.Close()
}
|
讲解
- 支持日志写入文件(
app.log
)。
- 使用
sync.Mutex
保护文件写入,确保并发安全。
- 提供
Close
方法清理资源。
常见问题
-
为什么不用全局变量?
全局变量在并发场景下不安全,且容易被外部修改,破坏封装性。
-
sync.Once 和 Mutex 的区别?
sync.Once
用于“只执行一次”的场景,代码简洁;Mutex适合多次修改共享资源。
-
单例模式在Go中常见吗?
不如Java中常见,但在日志、配置等场景中很有用。
总结
单例模式在Go中通过包、sync.Once
等实现,适合全局资源管理。本文通过日志管理器的例子,展示了非线程安全和线程安全的实现方式,以及在实际项目中的应用。希望这篇文章能帮助您更好地理解和使用单例模式!
欢迎留言:分享您在Go项目中使用单例模式的经验!
评论 0